From 9b1c5c391d336313d57d9474f798a4934444d22c Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Sun, 14 Jun 2026 09:48:54 -0700 Subject: [PATCH] =?UTF-8?q?harness:=20fix=20py-vs-python3=20doc=20gap=20?= =?UTF-8?q?=E2=80=94=20add=20py.sh=20resolver,=20repoint=20skill/command?= =?UTF-8?q?=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill/command DOCS instructed Claude to run a bare `py ...`, which is the Windows py-launcher — absent on Linux/macOS (exit 127, hit on GURU-KALI). A blind py->python3 swap is wrong too: python3 is a broken MS Store shim on some Windows boxes where `py` is the correct launcher. Fix mirrors the resolution the .sh skill scripts already do: - New .claude/scripts/py.sh: picks the interpreter that actually RUNS — identity.json python.command first, then py -> python3 -> python, each validated with `-c 'import sys'` so the MS Store stub is skipped. exec's it. - Repointed all DOC invocations (10 files, ~70 sites) from `py ...` to `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ...` (incl. the `py -c` and `py -` heredoc forms in checkpoint.md / mailbox.md). - Left the .sh skill scripts untouched — they already resolve py/python/python3. - errorlog.md: marked the GURU-KALI entry RESOLVED. Depends on CLAUDETOOLS_ROOT (seeded by ensure-settings-env.py); py.sh also self-resolves the repo root via git/cwd as a fallback. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/commands/checkpoint.md | 2 +- .claude/commands/mailbox.md | 2 +- .claude/commands/recover.md | 8 +- .claude/scripts/py.sh | 31 ++ .claude/skills/b2/SKILL.md | 10 +- .claude/skills/bitdefender/SKILL.md | 8 +- .claude/skills/coord/SKILL.md | 2 +- .claude/skills/mailprotector/SKILL.md | 312 +++++++++--------- .../skills/mailprotector/references/api.md | 6 +- .claude/skills/memory-dream/SKILL.md | 290 ++++++++-------- .claude/skills/packetdial/SKILL.md | 230 ++++++------- errorlog.md | 2 +- 12 files changed, 467 insertions(+), 436 deletions(-) create mode 100755 .claude/scripts/py.sh diff --git a/.claude/commands/checkpoint.md b/.claude/commands/checkpoint.md index 45d936e..fca50d2 100644 --- a/.claude/commands/checkpoint.md +++ b/.claude/commands/checkpoint.md @@ -33,7 +33,7 @@ Please create a comprehensive git checkpoint with the following steps: # Ollama drafts the body; fallback to Claude if unavailable if [ -n "$OLLAMA" ]; then - BODY=$(py -c " + BODY=$(bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" -c " import urllib.request, json diff = open('C:/Users/guru/AppData/Local/Temp/checkpoint_diff.txt', encoding='utf-8').read() prompt = 'Write a git commit message BODY only (not the summary line). Imperative mood. What changed and why. No filler. Under 150 words.\n\nDIFF:\n' + diff diff --git a/.claude/commands/mailbox.md b/.claude/commands/mailbox.md index 0912a40..beb07bb 100644 --- a/.claude/commands/mailbox.md +++ b/.claude/commands/mailbox.md @@ -64,7 +64,7 @@ VAULT="$REPO_ROOT/.claude/scripts/vault.sh" All Graph calls go through this `py` helper. It reads the secret from the vault, caches the token, and exposes `get`/`post`. Reuse the pattern per command. ```bash -py - "$MAILBOX" "$1" <<'PY' +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" - "$MAILBOX" "$1" <<'PY' import os, sys, json, time, subprocess, urllib.request, urllib.parse, re sys.stdout.reconfigure(encoding='utf-8', errors='replace') MAILBOX = sys.argv[1] diff --git a/.claude/commands/recover.md b/.claude/commands/recover.md index 1806450..4e1d6de 100644 --- a/.claude/commands/recover.md +++ b/.claude/commands/recover.md @@ -22,7 +22,7 @@ Reconstruct a session log from a Claude Code or Grok transcript when a session c Run the detector in scan-only mode and present the table to the user: ```bash -py .claude/scripts/detect_orphaned_sessions.py --dry-run +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .claude/scripts/detect_orphaned_sessions.py --dry-run ``` The table shows every past-idle, not-yet-processed transcript with its uuid, mtime, `substantive`/`saved`/`orphan` verdicts, classified scope, and the path a recovery would write to. Point the user at the rows where `orphan` is `YES` — those are unsaved substantive sessions. Nothing is written. @@ -36,7 +36,7 @@ This is a **reviewed** recovery. Claude is the editor, not a passive writer. 1. **Generate the draft** (prints to stdout, writes nothing): ```bash - py .claude/scripts/recover_session.py --uuid --print + bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .claude/scripts/recover_session.py --uuid --print ``` (or `--latest`). The draft contains: @@ -103,6 +103,6 @@ In `/recover` flows, if the chosen orphan or explicit id lives under a Grok sess Example (manual): ```bash -py .claude/scripts/recover_grok_session.py --latest --print -py .claude/scripts/recover_grok_session.py --uuid 019e8b67-f97e-7b33-9c45-ec34b342d3eb --auto +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .claude/scripts/recover_grok_session.py --latest --print +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .claude/scripts/recover_grok_session.py --uuid 019e8b67-f97e-7b33-9c45-ec34b342d3eb --auto ``` diff --git a/.claude/scripts/py.sh b/.claude/scripts/py.sh new file mode 100755 index 0000000..a0fe993 --- /dev/null +++ b/.claude/scripts/py.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Purpose: Resolve the Python interpreter that actually WORKS on this machine and exec it +# with the given args. Skill/command docs call this instead of a bare `py` so the +# same documented command runs everywhere. +# Why: `py` is the Windows py-launcher (absent on Linux/macOS -> exit 127), while +# `python3` is a broken MS Store shim on some Windows boxes. Neither literal is +# portable. This picks the first interpreter that genuinely executes. +# Usage: bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" [args...] +# (mirrors the py/python/python3 fallback already used inside the .sh skill scripts) +# Origin: added 2026-06-14 to close the py-vs-python3 doc gap (errorlog.md, GURU-KALI). +set -u + +# A candidate is only valid if it RUNS — this skips the MS Store python3 stub, which +# exists on PATH but exits non-zero (prompting a store install) instead of executing. +_works() { command -v "$1" >/dev/null 2>&1 && "$1" -c 'import sys' >/dev/null 2>&1; } + +PY="" +# 1) Honor identity.json's declared command first (authoritative per-machine), if it works. +ROOT="${CLAUDETOOLS_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" +ID="$ROOT/.claude/identity.json" +if [ -f "$ID" ]; then + cand=$(sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$ID" | head -1) + [ -n "$cand" ] && _works "$cand" && PY="$cand" +fi +# 2) Fall back to autodetect: py (Win launcher) -> python3 -> python, first that works. +if [ -z "$PY" ]; then + for c in py python3 python; do _works "$c" && { PY="$c"; break; }; done +fi + +[ -z "$PY" ] && { echo "[py.sh] no working python found (tried identity.json, py, python3, python)" >&2; exit 127; } +exec "$PY" "$@" diff --git a/.claude/skills/b2/SKILL.md b/.claude/skills/b2/SKILL.md index e4f8c4c..da57bc4 100644 --- a/.claude/skills/b2/SKILL.md +++ b/.claude/skills/b2/SKILL.md @@ -25,9 +25,9 @@ work with `python`/`python3`. ```bash # from the scripts dir, or pass full paths -py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" status -py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" buckets -py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" usage --json +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" status +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" buckets +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" usage --json ``` Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib` @@ -95,7 +95,7 @@ into tickets/logs. Destructive calls are NEVER retried automatically. ## Common commands ```bash -B2="py $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" +B2="bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" # Status / inventory $B2 status @@ -175,7 +175,7 @@ that would exceed that. ### Commands ```bash -B2="py $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" +B2="bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" # 1. READ-ONLY: see the bucket's current lifecycle rules + revision $B2 lifecycle ACG-Internal diff --git a/.claude/skills/bitdefender/SKILL.md b/.claude/skills/bitdefender/SKILL.md index b6985a8..559f152 100644 --- a/.claude/skills/bitdefender/SKILL.md +++ b/.claude/skills/bitdefender/SKILL.md @@ -27,9 +27,9 @@ work with `python`/`python3`. ```bash # from the scripts dir, or pass full paths -py "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" status -py "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" companies -py "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" sweep --company --json +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" status +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" companies +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" sweep --company --json ``` Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib` @@ -105,7 +105,7 @@ raw output into tickets/logs without review. ## Common commands ```bash -GZ="py C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" +GZ="bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" # Status / inventory $GZ status diff --git a/.claude/skills/coord/SKILL.md b/.claude/skills/coord/SKILL.md index 36c4add..399bc4c 100644 --- a/.claude/skills/coord/SKILL.md +++ b/.claude/skills/coord/SKILL.md @@ -19,7 +19,7 @@ and the `user`/`machine` for attribution) and bakes in the fleet conventions, so never hand-build the JSON again. ``` -py "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py" ... +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py" ... ``` | Command | What it does | diff --git a/.claude/skills/mailprotector/SKILL.md b/.claude/skills/mailprotector/SKILL.md index c556702..c3f8f4b 100644 --- a/.claude/skills/mailprotector/SKILL.md +++ b/.claude/skills/mailprotector/SKILL.md @@ -1,156 +1,156 @@ ---- -name: mailprotector -description: "Manage the ACG Mailprotector CloudFilter email-security gateway (emailservice.io). Search/release held/quarantined mail (in+outbound), pull mail-flow logs (why a message did/did not deliver), inspect + manage allow/block rules. Read-only default; releases/rule-changes gated --confirm. Triggers: mailprotector, cloudfilter, held/quarantined mail, release email, allow/block rule, INKY. Live production." - ---- - -# Mailprotector / CloudFilter Skill - -Standalone CLI client for the **Mailprotector CloudFilter REST API** -(`emailservice.io`), the reseller email-security platform ACG layers on top of -client mail flow. Read-only by default; every write (release, rule add, config -change) is gated behind `--confirm`. - -## The two-layer context (important) - -ACG's email security sits in front of client mailboxes as two cooperating layers: - -| Layer | What it does | -|---|---| -| **Mailprotector CloudFilter** | The delivery / filtering gateway. Inbound and outbound mail passes through it; spam, virus, and policy hits are **held / quarantined** here. Releasing a held message re-injects it for delivery. This is the API this skill drives. | -| **INKY** | Email annotation / phishing-banner layer. Adds the warning banners and protects against impersonation. Not part of this API surface. | - -Both sit **layered on top of the client's own Exchange / M365 mail flow** — so a -"missing email" investigation usually means: was it held at CloudFilter (check -`messages` / `logs`), or did it pass CloudFilter and stall in Exchange? - -## Connection - -| Item | Value | -|---|---| -| Base URL | `https://emailservice.io/api/v1` (override `MAILPROTECTOR_API_BASE_URL`) | -| Auth | `Authorization: Bearer ` | -| Vault entry | `msp-tools/mailprotector.sops.yaml`, field `credentials.api_key` | -| Env override | `MAILPROTECTOR_API_KEY` | - -Credential resolution order: `MAILPROTECTOR_API_KEY` env -> vault -`credentials.api_key`. The key is never hardcoded; a clear setup error is raised -if neither resolves. - -### Scopes - -Five entity types carry `logs` / `messages` / `configuration` / -`allow_block_rules` / `users` / `domains` sub-resources. Path form is -`/{scope}/{id}/...`: - -``` -resellers, customers, domains, user_groups, users -``` - -The CLI validates `scope` against this set. - -## Running the CLI - -This machine's Python launcher is `py` (per identity.json); `python` / `python3` -also work. Run from the scripts dir so the two modules resolve. - -```bash -cd C:/claudetools/.claude/skills/mailprotector/scripts - -py mp.py status # validate token (GET /domains, per_page=1) -py mp.py domains # list domains (global) -py mp.py domains --scope customers --id -py mp.py domain -py mp.py customers -py mp.py customer -py mp.py users -py mp.py user -py mp.py find-user user@client.com # locate a user / alias by email (a READ) -py mp.py config # shows permissions.messages.allow_spam_release -py mp.py rules -``` - -### Mail-flow logs and held mail (the common investigation) - -Both accept the same filters: `--sender --recipient --subject --decision ---sort-field --sort-direction --page --page-size`. - -```bash -# Why didn't this arrive? Look at the decision in the flow logs. -py mp.py logs domains --recipient ceo@client.com --decision quarantine_spam - -# Held / quarantined mail search. -py mp.py messages domains --sender boss@vendor.com -``` - -`--decision` values: `default`, `deliver`, `quarantine_spam`, -`quarantine_virus`, `quarantine_policy`, `bounce`, `encrypt`, `delete`. -`--sort-field` values: `@timestamp` (default), `prime.direction`, -`prime.from_header_raw`, `prime.recipient`, `prime.subject`, `prime.decision`, -`prime.score`. - -## Writes (gated) - -Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you -pass `--confirm`. - -```bash -py mp.py release --confirm -py mp.py release --recipients alt@client.com --confirm -py mp.py release-many --ids 111,222,333 --confirm -py mp.py release-many --all --confirm -py mp.py add-rule --value vendor.com --type allow --confirm -py mp.py enable-release --confirm -``` - -## The `allow_spam_release` gotcha - -Releasing a held **spam** message fails if the owning entity does not have -`permissions.messages.allow_spam_release = true`. Workflow: - -1. `py mp.py config ` — check `allow_spam_release`. -2. If `false`: `py mp.py enable-release --confirm`. -3. Re-run the `release` / `release-many`. - -Virus and policy quarantines are governed separately — only spam release is -gated by this permission. - -## Example workflow: find a client's held outbound mail from a sender and release it - -```bash -# 1. Find the client's domain. -py mp.py domains --scope customers --id - -# 2. Search held messages from the sender (outbound = sender is the client user). -py mp.py messages domains --sender user@client.com --decision quarantine_spam - -# 3. If it's spam-held, make sure release is permitted on the domain. -py mp.py config domains # check allow_spam_release -py mp.py enable-release domains --confirm # only if needed - -# 4. Release by message id (DRY RUN first — omit --confirm to preview). -py mp.py release # [DRY RUN] -py mp.py release --confirm # actually release -``` - -## Raw escape hatch - -The named commands cover the common surface; for anything else, hit the path -directly. Non-GET methods still require `--confirm`. - -```bash -py mp.py raw GET domains//logs -py mp.py raw POST messages//deliver --body '{"include_original_recipients":1}' --confirm -``` - -## Notes - -- This is the **LIVE production reseller CloudFilter platform**. A release - re-delivers real mail to real recipients, and an allow rule can let real spam - or phishing through — confirm the target entity with a read command before any - write, and prefer releasing specific message ids over `--all`. -- Pagination: `page` (default 1) and `per_page` (default 25); reseller - `messages` caps `per_page` at 50. The `X-Pagination` response header carries - the page/total metadata. -- Full endpoint catalog, filter tables, and the global `field[op]=value` - operators live in `references/api.md`. +--- +name: mailprotector +description: "Manage the ACG Mailprotector CloudFilter email-security gateway (emailservice.io). Search/release held/quarantined mail (in+outbound), pull mail-flow logs (why a message did/did not deliver), inspect + manage allow/block rules. Read-only default; releases/rule-changes gated --confirm. Triggers: mailprotector, cloudfilter, held/quarantined mail, release email, allow/block rule, INKY. Live production." + +--- + +# Mailprotector / CloudFilter Skill + +Standalone CLI client for the **Mailprotector CloudFilter REST API** +(`emailservice.io`), the reseller email-security platform ACG layers on top of +client mail flow. Read-only by default; every write (release, rule add, config +change) is gated behind `--confirm`. + +## The two-layer context (important) + +ACG's email security sits in front of client mailboxes as two cooperating layers: + +| Layer | What it does | +|---|---| +| **Mailprotector CloudFilter** | The delivery / filtering gateway. Inbound and outbound mail passes through it; spam, virus, and policy hits are **held / quarantined** here. Releasing a held message re-injects it for delivery. This is the API this skill drives. | +| **INKY** | Email annotation / phishing-banner layer. Adds the warning banners and protects against impersonation. Not part of this API surface. | + +Both sit **layered on top of the client's own Exchange / M365 mail flow** — so a +"missing email" investigation usually means: was it held at CloudFilter (check +`messages` / `logs`), or did it pass CloudFilter and stall in Exchange? + +## Connection + +| Item | Value | +|---|---| +| Base URL | `https://emailservice.io/api/v1` (override `MAILPROTECTOR_API_BASE_URL`) | +| Auth | `Authorization: Bearer ` | +| Vault entry | `msp-tools/mailprotector.sops.yaml`, field `credentials.api_key` | +| Env override | `MAILPROTECTOR_API_KEY` | + +Credential resolution order: `MAILPROTECTOR_API_KEY` env -> vault +`credentials.api_key`. The key is never hardcoded; a clear setup error is raised +if neither resolves. + +### Scopes + +Five entity types carry `logs` / `messages` / `configuration` / +`allow_block_rules` / `users` / `domains` sub-resources. Path form is +`/{scope}/{id}/...`: + +``` +resellers, customers, domains, user_groups, users +``` + +The CLI validates `scope` against this set. + +## Running the CLI + +This machine's Python launcher is `py` (per identity.json); `python` / `python3` +also work. Run from the scripts dir so the two modules resolve. + +```bash +cd C:/claudetools/.claude/skills/mailprotector/scripts + +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py status # validate token (GET /domains, per_page=1) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains # list domains (global) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains --scope customers --id +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domain +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customers +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customer +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py users +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py user +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py find-user user@client.com # locate a user / alias by email (a READ) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config # shows permissions.messages.allow_spam_release +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py rules +``` + +### Mail-flow logs and held mail (the common investigation) + +Both accept the same filters: `--sender --recipient --subject --decision +--sort-field --sort-direction --page --page-size`. + +```bash +# Why didn't this arrive? Look at the decision in the flow logs. +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py logs domains --recipient ceo@client.com --decision quarantine_spam + +# Held / quarantined mail search. +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains --sender boss@vendor.com +``` + +`--decision` values: `default`, `deliver`, `quarantine_spam`, +`quarantine_virus`, `quarantine_policy`, `bounce`, `encrypt`, `delete`. +`--sort-field` values: `@timestamp` (default), `prime.direction`, +`prime.from_header_raw`, `prime.recipient`, `prime.subject`, `prime.decision`, +`prime.score`. + +## Writes (gated) + +Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you +pass `--confirm`. + +```bash +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release --recipients alt@client.com --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many --ids 111,222,333 --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many --all --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py add-rule --value vendor.com --type allow --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release --confirm +``` + +## The `allow_spam_release` gotcha + +Releasing a held **spam** message fails if the owning entity does not have +`permissions.messages.allow_spam_release = true`. Workflow: + +1. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config ` — check `allow_spam_release`. +2. If `false`: `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release --confirm`. +3. Re-run the `release` / `release-many`. + +Virus and policy quarantines are governed separately — only spam release is +gated by this permission. + +## Example workflow: find a client's held outbound mail from a sender and release it + +```bash +# 1. Find the client's domain. +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains --scope customers --id + +# 2. Search held messages from the sender (outbound = sender is the client user). +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains --sender user@client.com --decision quarantine_spam + +# 3. If it's spam-held, make sure release is permitted on the domain. +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config domains # check allow_spam_release +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release domains --confirm # only if needed + +# 4. Release by message id (DRY RUN first — omit --confirm to preview). +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release # [DRY RUN] +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release --confirm # actually release +``` + +## Raw escape hatch + +The named commands cover the common surface; for anything else, hit the path +directly. Non-GET methods still require `--confirm`. + +```bash +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw GET domains//logs +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw POST messages//deliver --body '{"include_original_recipients":1}' --confirm +``` + +## Notes + +- This is the **LIVE production reseller CloudFilter platform**. A release + re-delivers real mail to real recipients, and an allow rule can let real spam + or phishing through — confirm the target entity with a read command before any + write, and prefer releasing specific message ids over `--all`. +- Pagination: `page` (default 1) and `per_page` (default 25); reseller + `messages` caps `per_page` at 50. The `X-Pagination` response header carries + the page/total metadata. +- Full endpoint catalog, filter tables, and the global `field[op]=value` + operators live in `references/api.md`. diff --git a/.claude/skills/mailprotector/references/api.md b/.claude/skills/mailprotector/references/api.md index 9e9bdcc..5be47ee 100644 --- a/.claude/skills/mailprotector/references/api.md +++ b/.claude/skills/mailprotector/references/api.md @@ -137,7 +137,7 @@ state first with `config ` and look at ## Raw escape hatch ``` -py mp.py raw [--body JSON] [--confirm] +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw [--body JSON] [--confirm] ``` Non-GET methods require `--confirm`. Use for any endpoint not wrapped by a named command. @@ -147,8 +147,8 @@ command. Releasing a held **spam** message will fail (or silently no-op) if the owning entity does not have `permissions.messages.allow_spam_release = true`. The fix: -1. `py mp.py config ` — confirm `allow_spam_release` is `false`. -2. `py mp.py enable-release --confirm` — flip it to `true`. +1. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config ` — confirm `allow_spam_release` is `false`. +2. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release --confirm` — flip it to `true`. 3. Re-run the `release` / `release-many`. Virus and policy quarantines are governed separately — only spam release is diff --git a/.claude/skills/memory-dream/SKILL.md b/.claude/skills/memory-dream/SKILL.md index 54eaaa6..caa93db 100644 --- a/.claude/skills/memory-dream/SKILL.md +++ b/.claude/skills/memory-dream/SKILL.md @@ -1,145 +1,145 @@ ---- -name: memory-dream -description: "Lint + consolidate the ClaudeTools repo memory store (.claude/memory/): audits index, backlinks, file paths, duplicate clusters, stale facts. Read-only default; --apply-safe does low-risk fixes; merges/deletes surfaced as proposals. Triggers: memory dream, consolidate/lint/clean up/dedupe memory." - ---- - -# Memory Dream - -A read-only-by-default analyzer that flags issues in the shared memory store. -Mutating ops are gated behind `--apply-safe` (for low-risk fixes) or the -PROPOSED section (for judgment calls a human resolves by hand). - -## The two-store model (important) - -There are TWO separate memory stores on every machine: - -- REPO store -- `.claude/memory/` (88+ `*.md` files + `MEMORY.md` index). - Tracked in git, syncs to all machines via Gitea. **This is the source of - truth.** `CLAUDE.md` mandates writing here. -- HARNESS PROFILE store -- `$HOME/.claude/projects//memory/`. Machine - local, NOT in git, NOT synced. This is the store the Claude Code harness - auto-injects into the system prompt at session start. - -The two drift over time. `memory-dream` reports that drift in its report -section. The companion script `.claude/scripts/sync-memory.sh` is what -actually reconciles them: it runs in **mirror mode** (since 2026-06-02) — -repo is authoritative, profile is synced to match (deletions propagate; -repo content wins on conflict). PROFILE-side hygiene lives in -`sync-memory.sh`, not here. - -## What it checks - -`scripts/memory_dream.py` runs six READ-ONLY analyses over the REPO store: - -1. INDEX RECONCILE -- orphan files (no `MEMORY.md` line), index lines whose - target file is missing, and frontmatter `name:` vs filename signals. -2. BACKLINKS -- `[[name]]` references in bodies whose target slug has no file. -3. REFERENCED-ARTIFACT VALIDITY -- conservatively extracts repo-relative file - paths / script names from each body (backtick-wrapped single tokens only) - and flags ones not found in the repo. Reported as **verify**, never delete - (many are legitimately server-side or in sibling repos). -4. DUPLICATE / OVERLAP CLUSTERS -- groups memories by type + token-overlap / - shared slug-prefix and lists candidate mergeable clusters (e.g. the many - `feedback_syncro_*` files). **Proposes** merges; never performs them. -5. STALE DATED FACTS -- flags `project`-type memories with an "as of " - style claim older than ~6 months for re-verification. -6. DRIFT vs PROFILE STORE -- locates the harness profile memory dir for this - project and reports profile-only files (candidates to migrate INTO the repo) - and repo-only files (candidates to push OUT to profile). Report only. - -The report ends with a `## PROPOSED (needs human approval)` section that is -NEVER auto-applied. - -## Modes - -- Default (no flag) -- **REPORT ONLY. Mutates nothing.** Writes a timestamped - report to `.claude/memory/_reports/YYYY-MM-DD-HHMM-dream.md` (created if - missing) and prints it to stdout. -- `--apply-safe` -- performs ONLY additive, non-destructive fixes and prints - each action: - - (a) append missing index lines to `MEMORY.md` for orphan files, under the - correct `## ` header, never reordering or removing existing lines; - - (b) copy profile-only memory files INTO the repo store (additive - migration). If a same-named repo file already exists it is SKIPPED and the - conflict is reported -- it is never overwritten. -- `--no-file` -- print to stdout only; skip writing the `_reports/` file. -- `--report-file ` -- write the report to an explicit path. - -### What dream does NOT auto-do - -`memory-dream` does NOT, even with `--apply-safe`: - -- delete a repo memory file (cluster dedup is a judgment call — pick which file becomes canonical, fold the others' content, retire the originals deliberately); -- remove or reorder index lines (index cleanups are also surfaced as proposals); -- overwrite a file whose content differs; -- perform a proposed merge. - -These stay in the report's `## PROPOSED` section. The rationale isn't "never delete" any more (the fleet-wide additive safety net was dropped 2026-06-02; see `feedback_memory_sync_destructive_ok.md`) — it's that merges and dedups require human judgment about which file is canonical and how to combine content. Profile-side deletion DOES happen automatically — but in `sync-memory.sh`, not here. - -### The operator MUST execute consolidations — do not just propose and leave - -The script is additive-only **by design** (auto-merging would corrupt the store — see the trap below), but the SESSION running `/memory-dream` is NOT additive-only. **Executing the `## PROPOSED` consolidations is the expected work of a dream run, not a someday-maybe.** Parking proposals indefinitely is exactly what causes the fleet drift this skill exists to prevent. Each run, after reading the report: - -1. **Triage every `[MERGE?]` cluster with judgment** (the detector is a coarse net): - - **Intentional `X` + `X_history` (or `_archive`/`_detail`) splits are NOT duplicates** — current-state vs on-demand archive, cross-linked in frontmatter. Leave them. (The detector skips these as of 2026-06-11; older reports may still list them.) - - **Topically-clustered but distinct facts** (e.g. several `reference_gitea_*`) — merge into one topic file ONLY if genuinely redundant or it reads better as one; otherwise leave. - - **True duplicates / superseded files** — merge: pick the canonical file, fold in the others' unique content, `git rm` the retired ones, fix `MEMORY.md`. -2. **Deletions are first-class.** `git rm` of a retired memory is correct and propagates to every machine's profile store via `sync-memory.sh` mirror mode (repo authoritative). This is the fleet-consistency mechanism — use it. -3. **Commit + sync** so the consolidated store reaches the fleet. - -If a cluster genuinely needs a decision you can't make, leave it AND say so explicitly in your summary — don't silently skip the whole PROPOSED section. - -**The auto-merge trap (why the script stays additive):** blindly merging the detector's clusters destroys deliberate structure — verified 2026-06-11 when the flagged `project_*` / `project_*_history` pairs turned out to be intentional splits and the `gitea` / `syncro` clusters were distinct facts, not copies. Consolidation needs the operator's judgment; the script must never do it unattended. - -## Running it - -This machine's Python launcher is `py` (per identity.json); the script also -runs under `python` / `python3`. Stdlib only -- no pip deps. - -```bash -# REPORT ONLY (default) -- writes _reports/-dream.md and prints it -py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" - -# report to stdout only, write nothing -py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" --no-file - -# additive-only fixes (append orphan index lines, migrate profile-only files) -py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" --apply-safe -``` - -`CLAUDETOOLS_ROOT` resolves from the env var, else `claudetools_root` in -`.claude/identity.json`, else the repo root derived from the script's own -location -- no hardcoded drive letters. - -## Cleanup / approve workflow - -1. Run with no flag. Read the report (stdout or `_reports/-dream.md`). -2. Run `--apply-safe` to take the safe additive wins: orphan index lines get - added, profile-only memories get migrated into the repo (conflicts skipped - and reported). -3. Work the `## PROPOSED` section by hand: - - `[MERGE?]` -- decide whether to consolidate a cluster. If yes, author a new - combined memory (or set of files for a rule/history split), retire the - originals via `git rm`, update `MEMORY.md`. Deletions are now first-class - — `sync-memory.sh` mirror mode will propagate them to every profile store - on the next run. - - `[REVERIFY?]` -- confirm the dated fact still holds; update the body and - its date if it changed. - - `[STALE-REF?]` -- confirm the referenced path moved/renamed; repoint or - annotate. Many are legitimately server-side (`.service` units, `/opt/...`). - - `[INDEX-CLEANUP?]` / `[DRIFT-RESOLVE?]` -- human picks the winner. -4. Commit the repo store changes so they sync to the fleet via Gitea. - -## Self-test - -`scripts/selftest.py` runs the analyzer against a synthetic fixture memory -store in a temp dir and asserts each detector fires (orphan, missing target, -broken backlink, stale path, cluster, profile drift) and that `--apply-safe` -only touches the things it's supposed to (index appends + profile→repo copy -of new files; no deletions, no merges, no overwrites of differing content). -Run: - -```bash -py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/selftest.py" -``` +--- +name: memory-dream +description: "Lint + consolidate the ClaudeTools repo memory store (.claude/memory/): audits index, backlinks, file paths, duplicate clusters, stale facts. Read-only default; --apply-safe does low-risk fixes; merges/deletes surfaced as proposals. Triggers: memory dream, consolidate/lint/clean up/dedupe memory." + +--- + +# Memory Dream + +A read-only-by-default analyzer that flags issues in the shared memory store. +Mutating ops are gated behind `--apply-safe` (for low-risk fixes) or the +PROPOSED section (for judgment calls a human resolves by hand). + +## The two-store model (important) + +There are TWO separate memory stores on every machine: + +- REPO store -- `.claude/memory/` (88+ `*.md` files + `MEMORY.md` index). + Tracked in git, syncs to all machines via Gitea. **This is the source of + truth.** `CLAUDE.md` mandates writing here. +- HARNESS PROFILE store -- `$HOME/.claude/projects//memory/`. Machine + local, NOT in git, NOT synced. This is the store the Claude Code harness + auto-injects into the system prompt at session start. + +The two drift over time. `memory-dream` reports that drift in its report +section. The companion script `.claude/scripts/sync-memory.sh` is what +actually reconciles them: it runs in **mirror mode** (since 2026-06-02) — +repo is authoritative, profile is synced to match (deletions propagate; +repo content wins on conflict). PROFILE-side hygiene lives in +`sync-memory.sh`, not here. + +## What it checks + +`scripts/memory_dream.py` runs six READ-ONLY analyses over the REPO store: + +1. INDEX RECONCILE -- orphan files (no `MEMORY.md` line), index lines whose + target file is missing, and frontmatter `name:` vs filename signals. +2. BACKLINKS -- `[[name]]` references in bodies whose target slug has no file. +3. REFERENCED-ARTIFACT VALIDITY -- conservatively extracts repo-relative file + paths / script names from each body (backtick-wrapped single tokens only) + and flags ones not found in the repo. Reported as **verify**, never delete + (many are legitimately server-side or in sibling repos). +4. DUPLICATE / OVERLAP CLUSTERS -- groups memories by type + token-overlap / + shared slug-prefix and lists candidate mergeable clusters (e.g. the many + `feedback_syncro_*` files). **Proposes** merges; never performs them. +5. STALE DATED FACTS -- flags `project`-type memories with an "as of " + style claim older than ~6 months for re-verification. +6. DRIFT vs PROFILE STORE -- locates the harness profile memory dir for this + project and reports profile-only files (candidates to migrate INTO the repo) + and repo-only files (candidates to push OUT to profile). Report only. + +The report ends with a `## PROPOSED (needs human approval)` section that is +NEVER auto-applied. + +## Modes + +- Default (no flag) -- **REPORT ONLY. Mutates nothing.** Writes a timestamped + report to `.claude/memory/_reports/YYYY-MM-DD-HHMM-dream.md` (created if + missing) and prints it to stdout. +- `--apply-safe` -- performs ONLY additive, non-destructive fixes and prints + each action: + - (a) append missing index lines to `MEMORY.md` for orphan files, under the + correct `## ` header, never reordering or removing existing lines; + - (b) copy profile-only memory files INTO the repo store (additive + migration). If a same-named repo file already exists it is SKIPPED and the + conflict is reported -- it is never overwritten. +- `--no-file` -- print to stdout only; skip writing the `_reports/` file. +- `--report-file ` -- write the report to an explicit path. + +### What dream does NOT auto-do + +`memory-dream` does NOT, even with `--apply-safe`: + +- delete a repo memory file (cluster dedup is a judgment call — pick which file becomes canonical, fold the others' content, retire the originals deliberately); +- remove or reorder index lines (index cleanups are also surfaced as proposals); +- overwrite a file whose content differs; +- perform a proposed merge. + +These stay in the report's `## PROPOSED` section. The rationale isn't "never delete" any more (the fleet-wide additive safety net was dropped 2026-06-02; see `feedback_memory_sync_destructive_ok.md`) — it's that merges and dedups require human judgment about which file is canonical and how to combine content. Profile-side deletion DOES happen automatically — but in `sync-memory.sh`, not here. + +### The operator MUST execute consolidations — do not just propose and leave + +The script is additive-only **by design** (auto-merging would corrupt the store — see the trap below), but the SESSION running `/memory-dream` is NOT additive-only. **Executing the `## PROPOSED` consolidations is the expected work of a dream run, not a someday-maybe.** Parking proposals indefinitely is exactly what causes the fleet drift this skill exists to prevent. Each run, after reading the report: + +1. **Triage every `[MERGE?]` cluster with judgment** (the detector is a coarse net): + - **Intentional `X` + `X_history` (or `_archive`/`_detail`) splits are NOT duplicates** — current-state vs on-demand archive, cross-linked in frontmatter. Leave them. (The detector skips these as of 2026-06-11; older reports may still list them.) + - **Topically-clustered but distinct facts** (e.g. several `reference_gitea_*`) — merge into one topic file ONLY if genuinely redundant or it reads better as one; otherwise leave. + - **True duplicates / superseded files** — merge: pick the canonical file, fold in the others' unique content, `git rm` the retired ones, fix `MEMORY.md`. +2. **Deletions are first-class.** `git rm` of a retired memory is correct and propagates to every machine's profile store via `sync-memory.sh` mirror mode (repo authoritative). This is the fleet-consistency mechanism — use it. +3. **Commit + sync** so the consolidated store reaches the fleet. + +If a cluster genuinely needs a decision you can't make, leave it AND say so explicitly in your summary — don't silently skip the whole PROPOSED section. + +**The auto-merge trap (why the script stays additive):** blindly merging the detector's clusters destroys deliberate structure — verified 2026-06-11 when the flagged `project_*` / `project_*_history` pairs turned out to be intentional splits and the `gitea` / `syncro` clusters were distinct facts, not copies. Consolidation needs the operator's judgment; the script must never do it unattended. + +## Running it + +This machine's Python launcher is `py` (per identity.json); the script also +runs under `python` / `python3`. Stdlib only -- no pip deps. + +```bash +# REPORT ONLY (default) -- writes _reports/-dream.md and prints it +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" + +# report to stdout only, write nothing +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" --no-file + +# additive-only fixes (append orphan index lines, migrate profile-only files) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" --apply-safe +``` + +`CLAUDETOOLS_ROOT` resolves from the env var, else `claudetools_root` in +`.claude/identity.json`, else the repo root derived from the script's own +location -- no hardcoded drive letters. + +## Cleanup / approve workflow + +1. Run with no flag. Read the report (stdout or `_reports/-dream.md`). +2. Run `--apply-safe` to take the safe additive wins: orphan index lines get + added, profile-only memories get migrated into the repo (conflicts skipped + and reported). +3. Work the `## PROPOSED` section by hand: + - `[MERGE?]` -- decide whether to consolidate a cluster. If yes, author a new + combined memory (or set of files for a rule/history split), retire the + originals via `git rm`, update `MEMORY.md`. Deletions are now first-class + — `sync-memory.sh` mirror mode will propagate them to every profile store + on the next run. + - `[REVERIFY?]` -- confirm the dated fact still holds; update the body and + its date if it changed. + - `[STALE-REF?]` -- confirm the referenced path moved/renamed; repoint or + annotate. Many are legitimately server-side (`.service` units, `/opt/...`). + - `[INDEX-CLEANUP?]` / `[DRIFT-RESOLVE?]` -- human picks the winner. +4. Commit the repo store changes so they sync to the fleet via Gitea. + +## Self-test + +`scripts/selftest.py` runs the analyzer against a synthetic fixture memory +store in a temp dir and asserts each detector fires (orphan, missing target, +broken backlink, stale path, cluster, profile drift) and that `--apply-safe` +only touches the things it's supposed to (index appends + profile→repo copy +of new files; no deletions, no merges, no overwrites of differing content). +Run: + +```bash +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/selftest.py" +``` diff --git a/.claude/skills/packetdial/SKILL.md b/.claude/skills/packetdial/SKILL.md index 373a40c..550386f 100644 --- a/.claude/skills/packetdial/SKILL.md +++ b/.claude/skills/packetdial/SKILL.md @@ -1,115 +1,115 @@ ---- -name: packetdial -description: "Manage the ACG PacketDial/OITVOIP hosted VoIP via the NetSapiens API (pbx.packetdial.com). List/inspect domains, users, devices, DIDs, resellers; pull CDRs; provision domains/users/SIP/numbers (writes gated --confirm; read-only default). Triggers: packetdial, oitvoip, netsapiens, voip domain/user/extension, provision phone, add did, CDR. Live production PBX." - ---- - -# PacketDial / NetSapiens (OITVOIP) Skill - -Standalone CLI client for the NetSapiens SNAPsolution **API v2** that backs -ACG's hosted-VoIP offering through OITVOIP / PacketDial. Read-only by default; -every write (create / update / delete) is gated behind `--confirm`. - -## The two hostnames (important) - -| Host | What it is | API? | -|---|---|---| -| `voip.packetdial.com` | Customer-facing white-label portal / UC & fax dashboard (e.g. Cascades fax account **28598**). Login-gated UI. | **No** | -| `pbx.packetdial.com` | Reseller PBX platform — NetSapiens v44.4. | **Yes** — this skill targets it | - -- API base: `https://pbx.packetdial.com/ns-api/v2` -- Token endpoint: `https://pbx.packetdial.com/ns-api/v2/tokens` -- Live OpenAPI spec: `https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json` -- Live Swagger UI: `https://pbx.packetdial.com/ns-api/openapi` -- Vendor docs: https://docs.ns-api.com/ (login) and https://voipdocs.io/oitvoip-access-platform-apis - -## Credentials — ONE-TIME SETUP (not yet provisioned) - -As of this skill's creation **no API key exists yet** — the vault entry -`msp-tools/oitvoip.sops.yaml` is empty/absent, so every command will fail with a -clear "No credentials found" error until you do this once: - -1. Log into `pbx.packetdial.com` -> **Admin > API Keys** and create a - reseller-scoped key (prefix `nsr_`). If self-service key creation is not - available, reply to **Darwin Escaro (OITVOIP)** for reseller OAuth client - credentials. -2. Store it in the SOPS vault. Preferred (static bearer key): - ``` - # msp-tools/oitvoip.sops.yaml - credentials: - api_key: nsr_xxxxxxxxxxxxxxxx - ``` - Or, for OAuth2 password-grant credentials: - ``` - credentials: - client_id: - client_secret: - username: - password: - ``` -3. That's it — the client auto-detects which shape is present. - -The client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env --> `PACKETDIAL_CLIENT_ID`+friends env -> vault `credentials.api_key` -> vault -OAuth fields. Env overrides exist for quick testing without touching the vault. - -## Running the CLI - -This machine's Python launcher is `py` (per identity.json); `python` / `python3` -also work. Run from the scripts dir so the two modules resolve. - -```bash -cd C:/claudetools/.claude/skills/packetdial/scripts - -py ns.py status # API version + authenticated key identity -py ns.py domains # list all domains -py ns.py domain # one domain's config -py ns.py users # users / extensions in a domain -py ns.py user -py ns.py phones # SIP devices registered in a domain -py ns.py dids # phone numbers (DIDs) on a domain -py ns.py devices -py ns.py cdrs --domain --start 2026-06-01 --end 2026-06-02 -py ns.py resellers -``` - -## Writes (gated) - -Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you -pass `--confirm`. Bodies are raw JSON matching the NetSapiens v2 schema. - -```bash -py ns.py create-domain --body '{"domain":"acme","description":"Acme Inc"}' --confirm -py ns.py create-user acme --body '{"user":"101","name-first-name":"Jane"}' --confirm -py ns.py create-phone acme --body '{...}' --confirm -py ns.py create-did acme --body '{"phonenumber":"15205551234"}' --confirm -py ns.py update-user acme 101 --body '{"name-last-name":"Doe"}' --confirm -py ns.py delete-user acme 101 --confirm -``` - -## Raw escape hatch (any of the 239 v2 paths) - -The named commands cover the common surface; for anything else, hit the path -directly. Non-GET methods still require `--confirm`. - -```bash -py ns.py raw GET domains/acme/users/101/answerrules -py ns.py raw POST domains/acme/users --body '{...}' --confirm -``` - -## Standard provisioning flow (new customer) - -1. `create-domain` -> dial plan auto-generates -2. `create-user` per extension -3. `create-phone` per SIP device (MAC-provisioned) -4. `create-did` to attach DIDs and route them to users -5. Log the work back to the Syncro ticket - -## Notes - -- This is the LIVE production reseller PBX. A bad `create-domain` or - `delete-user` affects real customers — confirm the target domain first with a - read command before any write. -- CDR queries can be large; always pass `--start`/`--end` and a `--limit`. -- Reference detail (auth shapes, full endpoint inventory) lives in - `references/api.md`. +--- +name: packetdial +description: "Manage the ACG PacketDial/OITVOIP hosted VoIP via the NetSapiens API (pbx.packetdial.com). List/inspect domains, users, devices, DIDs, resellers; pull CDRs; provision domains/users/SIP/numbers (writes gated --confirm; read-only default). Triggers: packetdial, oitvoip, netsapiens, voip domain/user/extension, provision phone, add did, CDR. Live production PBX." + +--- + +# PacketDial / NetSapiens (OITVOIP) Skill + +Standalone CLI client for the NetSapiens SNAPsolution **API v2** that backs +ACG's hosted-VoIP offering through OITVOIP / PacketDial. Read-only by default; +every write (create / update / delete) is gated behind `--confirm`. + +## The two hostnames (important) + +| Host | What it is | API? | +|---|---|---| +| `voip.packetdial.com` | Customer-facing white-label portal / UC & fax dashboard (e.g. Cascades fax account **28598**). Login-gated UI. | **No** | +| `pbx.packetdial.com` | Reseller PBX platform — NetSapiens v44.4. | **Yes** — this skill targets it | + +- API base: `https://pbx.packetdial.com/ns-api/v2` +- Token endpoint: `https://pbx.packetdial.com/ns-api/v2/tokens` +- Live OpenAPI spec: `https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json` +- Live Swagger UI: `https://pbx.packetdial.com/ns-api/openapi` +- Vendor docs: https://docs.ns-api.com/ (login) and https://voipdocs.io/oitvoip-access-platform-apis + +## Credentials — ONE-TIME SETUP (not yet provisioned) + +As of this skill's creation **no API key exists yet** — the vault entry +`msp-tools/oitvoip.sops.yaml` is empty/absent, so every command will fail with a +clear "No credentials found" error until you do this once: + +1. Log into `pbx.packetdial.com` -> **Admin > API Keys** and create a + reseller-scoped key (prefix `nsr_`). If self-service key creation is not + available, reply to **Darwin Escaro (OITVOIP)** for reseller OAuth client + credentials. +2. Store it in the SOPS vault. Preferred (static bearer key): + ``` + # msp-tools/oitvoip.sops.yaml + credentials: + api_key: nsr_xxxxxxxxxxxxxxxx + ``` + Or, for OAuth2 password-grant credentials: + ``` + credentials: + client_id: + client_secret: + username: + password: + ``` +3. That's it — the client auto-detects which shape is present. + +The client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env +-> `PACKETDIAL_CLIENT_ID`+friends env -> vault `credentials.api_key` -> vault +OAuth fields. Env overrides exist for quick testing without touching the vault. + +## Running the CLI + +This machine's Python launcher is `py` (per identity.json); `python` / `python3` +also work. Run from the scripts dir so the two modules resolve. + +```bash +cd C:/claudetools/.claude/skills/packetdial/scripts + +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py status # API version + authenticated key identity +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domains # list all domains +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domain # one domain's config +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py users # users / extensions in a domain +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py user +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py phones # SIP devices registered in a domain +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dids # phone numbers (DIDs) on a domain +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py devices +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain --start 2026-06-01 --end 2026-06-02 +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py resellers +``` + +## Writes (gated) + +Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you +pass `--confirm`. Bodies are raw JSON matching the NetSapiens v2 schema. + +```bash +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-domain --body '{"domain":"acme","description":"Acme Inc"}' --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-user acme --body '{"user":"101","name-first-name":"Jane"}' --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-phone acme --body '{...}' --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-did acme --body '{"phonenumber":"15205551234"}' --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py update-user acme 101 --body '{"name-last-name":"Doe"}' --confirm +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py delete-user acme 101 --confirm +``` + +## Raw escape hatch (any of the 239 v2 paths) + +The named commands cover the common surface; for anything else, hit the path +directly. Non-GET methods still require `--confirm`. + +```bash +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET domains/acme/users/101/answerrules +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw POST domains/acme/users --body '{...}' --confirm +``` + +## Standard provisioning flow (new customer) + +1. `create-domain` -> dial plan auto-generates +2. `create-user` per extension +3. `create-phone` per SIP device (MAC-provisioned) +4. `create-did` to attach DIDs and route them to users +5. Log the work back to the Syncro ticket + +## Notes + +- This is the LIVE production reseller PBX. A bad `create-domain` or + `delete-user` affects real customers — confirm the target domain first with a + read command before any write. +- CDR queries can be large; always pass `--start`/`--end` and a `--limit`. +- Reference detail (auth shapes, full endpoint inventory) lives in + `references/api.md`. diff --git a/errorlog.md b/errorlog.md index ff67512..d99149f 100644 --- a/errorlog.md +++ b/errorlog.md @@ -9,7 +9,7 @@ Format: `YYYY-MM-DD | MACHINE | command/skill | error (brief)` -2026-06-14 | GURU-KALI | coord skill (coord.py) | Documented invocation `py .claude/skills/coord/scripts/coord.py ...` failed exit 127 — `py` (the Windows py-launcher) does not exist on Linux. Worked around with `python3`. Skill docs assume the `py` launcher; on Linux/macOS machines they need `python3`. Candidate: skill docs use a platform-neutral python resolver, or document `python3` for non-Windows. +2026-06-14 | GURU-KALI | coord skill (coord.py) | Documented invocation `py .claude/skills/coord/scripts/coord.py ...` failed exit 127 — `py` (the Windows py-launcher) does not exist on Linux. Worked around with `python3`. [RESOLVED 2026-06-14] Added `.claude/scripts/py.sh` (resolves the working interpreter: identity.json `python.command` -> py -> python3 -> python, skipping the MS Store shim) and repointed all skill/command DOC invocations from bare `py` to `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh"`. The `.sh` skill scripts already resolved internally — left untouched. Broadcast to fleet. 2026-06-14 | GURU-BEAST-ROG | coord skill (coord.py msg send) | `py "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py"` failed — `$CLAUDETOOLS_ROOT` is not exported in fresh Git-bash shells here, so the path resolved under `C:\Program Files\Git\`. [RESOLVED 2026-06-14] Added `.claude/scripts/ensure-settings-env.py` (seeds `env.CLAUDETOOLS_ROOT` in per-machine `settings.local.json` from `identity.json`); Claude Code injects it into every Bash call. Wired into ONBOARDING.md + broadcast to fleet. Effective next session start.