harness: fix py-vs-python3 doc gap — add py.sh resolver, repoint skill/command docs

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 09:48:54 -07:00
parent b0d280903e
commit e6056433a2
12 changed files with 467 additions and 436 deletions

View File

@@ -33,7 +33,7 @@ Please create a comprehensive git checkpoint with the following steps:
# Ollama drafts the body; fallback to Claude if unavailable # Ollama drafts the body; fallback to Claude if unavailable
if [ -n "$OLLAMA" ]; then if [ -n "$OLLAMA" ]; then
BODY=$(py -c " BODY=$(bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" -c "
import urllib.request, json import urllib.request, json
diff = open('C:/Users/guru/AppData/Local/Temp/checkpoint_diff.txt', encoding='utf-8').read() 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 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

View File

@@ -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. 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 ```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 import os, sys, json, time, subprocess, urllib.request, urllib.parse, re
sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stdout.reconfigure(encoding='utf-8', errors='replace')
MAILBOX = sys.argv[1] MAILBOX = sys.argv[1]

View File

@@ -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: Run the detector in scan-only mode and present the table to the user:
```bash ```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. 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): 1. **Generate the draft** (prints to stdout, writes nothing):
```bash ```bash
py .claude/scripts/recover_session.py --uuid <uuid> --print bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .claude/scripts/recover_session.py --uuid <uuid> --print
``` ```
(or `--latest`). The draft contains: (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): Example (manual):
```bash ```bash
py .claude/scripts/recover_grok_session.py --latest --print bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .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 --uuid 019e8b67-f97e-7b33-9c45-ec34b342d3eb --auto
``` ```

31
.claude/scripts/py.sh Executable file
View File

@@ -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" <script.py> [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" "$@"

View File

@@ -25,9 +25,9 @@ work with `python`/`python3`.
```bash ```bash
# from the scripts dir, or pass full paths # from the scripts dir, or pass full paths
py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" status bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" status
py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" buckets bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$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" usage --json
``` ```
Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib` 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 ## Common commands
```bash ```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 # Status / inventory
$B2 status $B2 status
@@ -175,7 +175,7 @@ that would exceed that.
### Commands ### Commands
```bash ```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 # 1. READ-ONLY: see the bucket's current lifecycle rules + revision
$B2 lifecycle ACG-Internal $B2 lifecycle ACG-Internal

View File

@@ -27,9 +27,9 @@ work with `python`/`python3`.
```bash ```bash
# from the scripts dir, or pass full paths # from the scripts dir, or pass full paths
py "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" status bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" status
py "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" companies bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" companies
py "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" sweep --company <id> --json bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "C:/claudetools/.claude/skills/bitdefender/scripts/gz.py" sweep --company <id> --json
``` ```
Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib` Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib`
@@ -105,7 +105,7 @@ raw output into tickets/logs without review.
## Common commands ## Common commands
```bash ```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 # Status / inventory
$GZ status $GZ status

View File

@@ -19,7 +19,7 @@ and the `user`/`machine` for attribution) and bakes in the fleet conventions, so
never hand-build the JSON again. never hand-build the JSON again.
``` ```
py "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py" <command> ... bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py" <command> ...
``` ```
| Command | What it does | | Command | What it does |

View File

@@ -1,156 +1,156 @@
--- ---
name: mailprotector 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." 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 # Mailprotector / CloudFilter Skill
Standalone CLI client for the **Mailprotector CloudFilter REST API** Standalone CLI client for the **Mailprotector CloudFilter REST API**
(`emailservice.io`), the reseller email-security platform ACG layers on top of (`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 client mail flow. Read-only by default; every write (release, rule add, config
change) is gated behind `--confirm`. change) is gated behind `--confirm`.
## The two-layer context (important) ## The two-layer context (important)
ACG's email security sits in front of client mailboxes as two cooperating layers: ACG's email security sits in front of client mailboxes as two cooperating layers:
| Layer | What it does | | 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. | | **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. | | **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 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 "missing email" investigation usually means: was it held at CloudFilter (check
`messages` / `logs`), or did it pass CloudFilter and stall in Exchange? `messages` / `logs`), or did it pass CloudFilter and stall in Exchange?
## Connection ## Connection
| Item | Value | | Item | Value |
|---|---| |---|---|
| Base URL | `https://emailservice.io/api/v1` (override `MAILPROTECTOR_API_BASE_URL`) | | Base URL | `https://emailservice.io/api/v1` (override `MAILPROTECTOR_API_BASE_URL`) |
| Auth | `Authorization: Bearer <api_key>` | | Auth | `Authorization: Bearer <api_key>` |
| Vault entry | `msp-tools/mailprotector.sops.yaml`, field `credentials.api_key` | | Vault entry | `msp-tools/mailprotector.sops.yaml`, field `credentials.api_key` |
| Env override | `MAILPROTECTOR_API_KEY` | | Env override | `MAILPROTECTOR_API_KEY` |
Credential resolution order: `MAILPROTECTOR_API_KEY` env -> vault Credential resolution order: `MAILPROTECTOR_API_KEY` env -> vault
`credentials.api_key`. The key is never hardcoded; a clear setup error is raised `credentials.api_key`. The key is never hardcoded; a clear setup error is raised
if neither resolves. if neither resolves.
### Scopes ### Scopes
Five entity types carry `logs` / `messages` / `configuration` / Five entity types carry `logs` / `messages` / `configuration` /
`allow_block_rules` / `users` / `domains` sub-resources. Path form is `allow_block_rules` / `users` / `domains` sub-resources. Path form is
`/{scope}/{id}/...`: `/{scope}/{id}/...`:
``` ```
resellers, customers, domains, user_groups, users resellers, customers, domains, user_groups, users
``` ```
The CLI validates `scope` against this set. The CLI validates `scope` against this set.
## Running the CLI ## Running the CLI
This machine's Python launcher is `py` (per identity.json); `python` / `python3` This machine's Python launcher is `py` (per identity.json); `python` / `python3`
also work. Run from the scripts dir so the two modules resolve. also work. Run from the scripts dir so the two modules resolve.
```bash ```bash
cd C:/claudetools/.claude/skills/mailprotector/scripts cd C:/claudetools/.claude/skills/mailprotector/scripts
py mp.py status # validate token (GET /domains, per_page=1) bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py status # validate token (GET /domains, per_page=1)
py mp.py domains # list domains (global) bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains # list domains (global)
py mp.py domains --scope customers --id <id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains --scope customers --id <id>
py mp.py domain <domain_id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domain <domain_id>
py mp.py customers <reseller_id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customers <reseller_id>
py mp.py customer <customer_id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customer <customer_id>
py mp.py users <scope> <id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py users <scope> <id>
py mp.py user <user_id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py user <user_id>
py mp.py find-user user@client.com # locate a user / alias by email (a READ) bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py find-user user@client.com # locate a user / alias by email (a READ)
py mp.py config <scope> <id> # shows permissions.messages.allow_spam_release bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config <scope> <id> # shows permissions.messages.allow_spam_release
py mp.py rules <scope> <id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py rules <scope> <id>
``` ```
### Mail-flow logs and held mail (the common investigation) ### Mail-flow logs and held mail (the common investigation)
Both accept the same filters: `--sender --recipient --subject --decision Both accept the same filters: `--sender --recipient --subject --decision
--sort-field --sort-direction --page --page-size`. --sort-field --sort-direction --page --page-size`.
```bash ```bash
# Why didn't this arrive? Look at the decision in the flow logs. # Why didn't this arrive? Look at the decision in the flow logs.
py mp.py logs domains <domain_id> --recipient ceo@client.com --decision quarantine_spam bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py logs domains <domain_id> --recipient ceo@client.com --decision quarantine_spam
# Held / quarantined mail search. # Held / quarantined mail search.
py mp.py messages domains <domain_id> --sender boss@vendor.com bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains <domain_id> --sender boss@vendor.com
``` ```
`--decision` values: `default`, `deliver`, `quarantine_spam`, `--decision` values: `default`, `deliver`, `quarantine_spam`,
`quarantine_virus`, `quarantine_policy`, `bounce`, `encrypt`, `delete`. `quarantine_virus`, `quarantine_policy`, `bounce`, `encrypt`, `delete`.
`--sort-field` values: `@timestamp` (default), `prime.direction`, `--sort-field` values: `@timestamp` (default), `prime.direction`,
`prime.from_header_raw`, `prime.recipient`, `prime.subject`, `prime.decision`, `prime.from_header_raw`, `prime.recipient`, `prime.subject`, `prime.decision`,
`prime.score`. `prime.score`.
## Writes (gated) ## Writes (gated)
Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you
pass `--confirm`. pass `--confirm`.
```bash ```bash
py mp.py release <message_id> --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --confirm
py mp.py release <message_id> --recipients alt@client.com --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --recipients alt@client.com --confirm
py mp.py release-many <scope> <id> --ids 111,222,333 --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many <scope> <id> --ids 111,222,333 --confirm
py mp.py release-many <scope> <id> --all --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many <scope> <id> --all --confirm
py mp.py add-rule <scope> <id> --value vendor.com --type allow --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py add-rule <scope> <id> --value vendor.com --type allow --confirm
py mp.py enable-release <scope> <id> --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --confirm
``` ```
## The `allow_spam_release` gotcha ## The `allow_spam_release` gotcha
Releasing a held **spam** message fails if the owning entity does not have Releasing a held **spam** message fails if the owning entity does not have
`permissions.messages.allow_spam_release = true`. Workflow: `permissions.messages.allow_spam_release = true`. Workflow:
1. `py mp.py config <scope> <id>` — check `allow_spam_release`. 1. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config <scope> <id>` — check `allow_spam_release`.
2. If `false`: `py mp.py enable-release <scope> <id> --confirm`. 2. If `false`: `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --confirm`.
3. Re-run the `release` / `release-many`. 3. Re-run the `release` / `release-many`.
Virus and policy quarantines are governed separately — only spam release is Virus and policy quarantines are governed separately — only spam release is
gated by this permission. gated by this permission.
## Example workflow: find a client's held outbound mail from a sender and release it ## Example workflow: find a client's held outbound mail from a sender and release it
```bash ```bash
# 1. Find the client's domain. # 1. Find the client's domain.
py mp.py domains --scope customers --id <customer_id> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains --scope customers --id <customer_id>
# 2. Search held messages from the sender (outbound = sender is the client user). # 2. Search held messages from the sender (outbound = sender is the client user).
py mp.py messages domains <domain_id> --sender user@client.com --decision quarantine_spam bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains <domain_id> --sender user@client.com --decision quarantine_spam
# 3. If it's spam-held, make sure release is permitted on the domain. # 3. If it's spam-held, make sure release is permitted on the domain.
py mp.py config domains <domain_id> # check allow_spam_release bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config domains <domain_id> # check allow_spam_release
py mp.py enable-release domains <domain_id> --confirm # only if needed bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release domains <domain_id> --confirm # only if needed
# 4. Release by message id (DRY RUN first — omit --confirm to preview). # 4. Release by message id (DRY RUN first — omit --confirm to preview).
py mp.py release <message_id> # [DRY RUN] bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> # [DRY RUN]
py mp.py release <message_id> --confirm # actually release bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --confirm # actually release
``` ```
## Raw escape hatch ## Raw escape hatch
The named commands cover the common surface; for anything else, hit the path The named commands cover the common surface; for anything else, hit the path
directly. Non-GET methods still require `--confirm`. directly. Non-GET methods still require `--confirm`.
```bash ```bash
py mp.py raw GET domains/<id>/logs bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw GET domains/<id>/logs
py mp.py raw POST messages/<id>/deliver --body '{"include_original_recipients":1}' --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw POST messages/<id>/deliver --body '{"include_original_recipients":1}' --confirm
``` ```
## Notes ## Notes
- This is the **LIVE production reseller CloudFilter platform**. A release - 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 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 or phishing through — confirm the target entity with a read command before any
write, and prefer releasing specific message ids over `--all`. write, and prefer releasing specific message ids over `--all`.
- Pagination: `page` (default 1) and `per_page` (default 25); reseller - Pagination: `page` (default 1) and `per_page` (default 25); reseller
`messages` caps `per_page` at 50. The `X-Pagination` response header carries `messages` caps `per_page` at 50. The `X-Pagination` response header carries
the page/total metadata. the page/total metadata.
- Full endpoint catalog, filter tables, and the global `field[op]=value` - Full endpoint catalog, filter tables, and the global `field[op]=value`
operators live in `references/api.md`. operators live in `references/api.md`.

View File

@@ -137,7 +137,7 @@ state first with `config <scope> <id>` and look at
## Raw escape hatch ## Raw escape hatch
``` ```
py mp.py raw <METHOD> <path> [--body JSON] [--confirm] bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw <METHOD> <path> [--body JSON] [--confirm]
``` ```
Non-GET methods require `--confirm`. Use for any endpoint not wrapped by a named Non-GET methods require `--confirm`. Use for any endpoint not wrapped by a named
command. command.
@@ -147,8 +147,8 @@ command.
Releasing a held **spam** message will fail (or silently no-op) if the owning 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: entity does not have `permissions.messages.allow_spam_release = true`. The fix:
1. `py mp.py config <scope> <id>` — confirm `allow_spam_release` is `false`. 1. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config <scope> <id>` — confirm `allow_spam_release` is `false`.
2. `py mp.py enable-release <scope> <id> --confirm` — flip it to `true`. 2. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --confirm` — flip it to `true`.
3. Re-run the `release` / `release-many`. 3. Re-run the `release` / `release-many`.
Virus and policy quarantines are governed separately — only spam release is Virus and policy quarantines are governed separately — only spam release is

View File

@@ -1,145 +1,145 @@
--- ---
name: memory-dream 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." 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 # Memory Dream
A read-only-by-default analyzer that flags issues in the shared memory store. 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 Mutating ops are gated behind `--apply-safe` (for low-risk fixes) or the
PROPOSED section (for judgment calls a human resolves by hand). PROPOSED section (for judgment calls a human resolves by hand).
## The two-store model (important) ## The two-store model (important)
There are TWO separate memory stores on every machine: There are TWO separate memory stores on every machine:
- REPO store -- `.claude/memory/` (88+ `*.md` files + `MEMORY.md` index). - REPO store -- `.claude/memory/` (88+ `*.md` files + `MEMORY.md` index).
Tracked in git, syncs to all machines via Gitea. **This is the source of Tracked in git, syncs to all machines via Gitea. **This is the source of
truth.** `CLAUDE.md` mandates writing here. truth.** `CLAUDE.md` mandates writing here.
- HARNESS PROFILE store -- `$HOME/.claude/projects/<slug>/memory/`. Machine - HARNESS PROFILE store -- `$HOME/.claude/projects/<slug>/memory/`. Machine
local, NOT in git, NOT synced. This is the store the Claude Code harness local, NOT in git, NOT synced. This is the store the Claude Code harness
auto-injects into the system prompt at session start. auto-injects into the system prompt at session start.
The two drift over time. `memory-dream` reports that drift in its report The two drift over time. `memory-dream` reports that drift in its report
section. The companion script `.claude/scripts/sync-memory.sh` is what section. The companion script `.claude/scripts/sync-memory.sh` is what
actually reconciles them: it runs in **mirror mode** (since 2026-06-02) — actually reconciles them: it runs in **mirror mode** (since 2026-06-02) —
repo is authoritative, profile is synced to match (deletions propagate; repo is authoritative, profile is synced to match (deletions propagate;
repo content wins on conflict). PROFILE-side hygiene lives in repo content wins on conflict). PROFILE-side hygiene lives in
`sync-memory.sh`, not here. `sync-memory.sh`, not here.
## What it checks ## What it checks
`scripts/memory_dream.py` runs six READ-ONLY analyses over the REPO store: `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 1. INDEX RECONCILE -- orphan files (no `MEMORY.md` line), index lines whose
target file is missing, and frontmatter `name:` vs filename signals. target file is missing, and frontmatter `name:` vs filename signals.
2. BACKLINKS -- `[[name]]` references in bodies whose target slug has no file. 2. BACKLINKS -- `[[name]]` references in bodies whose target slug has no file.
3. REFERENCED-ARTIFACT VALIDITY -- conservatively extracts repo-relative file 3. REFERENCED-ARTIFACT VALIDITY -- conservatively extracts repo-relative file
paths / script names from each body (backtick-wrapped single tokens only) paths / script names from each body (backtick-wrapped single tokens only)
and flags ones not found in the repo. Reported as **verify**, never delete and flags ones not found in the repo. Reported as **verify**, never delete
(many are legitimately server-side or in sibling repos). (many are legitimately server-side or in sibling repos).
4. DUPLICATE / OVERLAP CLUSTERS -- groups memories by type + token-overlap / 4. DUPLICATE / OVERLAP CLUSTERS -- groups memories by type + token-overlap /
shared slug-prefix and lists candidate mergeable clusters (e.g. the many shared slug-prefix and lists candidate mergeable clusters (e.g. the many
`feedback_syncro_*` files). **Proposes** merges; never performs them. `feedback_syncro_*` files). **Proposes** merges; never performs them.
5. STALE DATED FACTS -- flags `project`-type memories with an "as of <date>" 5. STALE DATED FACTS -- flags `project`-type memories with an "as of <date>"
style claim older than ~6 months for re-verification. style claim older than ~6 months for re-verification.
6. DRIFT vs PROFILE STORE -- locates the harness profile memory dir for this 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) project and reports profile-only files (candidates to migrate INTO the repo)
and repo-only files (candidates to push OUT to profile). Report only. and repo-only files (candidates to push OUT to profile). Report only.
The report ends with a `## PROPOSED (needs human approval)` section that is The report ends with a `## PROPOSED (needs human approval)` section that is
NEVER auto-applied. NEVER auto-applied.
## Modes ## Modes
- Default (no flag) -- **REPORT ONLY. Mutates nothing.** Writes a timestamped - Default (no flag) -- **REPORT ONLY. Mutates nothing.** Writes a timestamped
report to `.claude/memory/_reports/YYYY-MM-DD-HHMM-dream.md` (created if report to `.claude/memory/_reports/YYYY-MM-DD-HHMM-dream.md` (created if
missing) and prints it to stdout. missing) and prints it to stdout.
- `--apply-safe` -- performs ONLY additive, non-destructive fixes and prints - `--apply-safe` -- performs ONLY additive, non-destructive fixes and prints
each action: each action:
- (a) append missing index lines to `MEMORY.md` for orphan files, under the - (a) append missing index lines to `MEMORY.md` for orphan files, under the
correct `## <Type>` header, never reordering or removing existing lines; correct `## <Type>` header, never reordering or removing existing lines;
- (b) copy profile-only memory files INTO the repo store (additive - (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 migration). If a same-named repo file already exists it is SKIPPED and the
conflict is reported -- it is never overwritten. conflict is reported -- it is never overwritten.
- `--no-file` -- print to stdout only; skip writing the `_reports/` file. - `--no-file` -- print to stdout only; skip writing the `_reports/` file.
- `--report-file <path>` -- write the report to an explicit path. - `--report-file <path>` -- write the report to an explicit path.
### What dream does NOT auto-do ### What dream does NOT auto-do
`memory-dream` does NOT, even with `--apply-safe`: `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); - 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); - remove or reorder index lines (index cleanups are also surfaced as proposals);
- overwrite a file whose content differs; - overwrite a file whose content differs;
- perform a proposed merge. - 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. 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 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: 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): 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.) - **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. - **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`. - **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. 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. 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. 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. **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 ## Running it
This machine's Python launcher is `py` (per identity.json); the script also This machine's Python launcher is `py` (per identity.json); the script also
runs under `python` / `python3`. Stdlib only -- no pip deps. runs under `python` / `python3`. Stdlib only -- no pip deps.
```bash ```bash
# REPORT ONLY (default) -- writes _reports/<stamp>-dream.md and prints it # REPORT ONLY (default) -- writes _reports/<stamp>-dream.md and prints it
py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py"
# report to stdout only, write nothing # report to stdout only, write nothing
py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" --no-file 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) # additive-only fixes (append orphan index lines, migrate profile-only files)
py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/memory_dream.py" --apply-safe 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 `CLAUDETOOLS_ROOT` resolves from the env var, else `claudetools_root` in
`.claude/identity.json`, else the repo root derived from the script's own `.claude/identity.json`, else the repo root derived from the script's own
location -- no hardcoded drive letters. location -- no hardcoded drive letters.
## Cleanup / approve workflow ## Cleanup / approve workflow
1. Run with no flag. Read the report (stdout or `_reports/<stamp>-dream.md`). 1. Run with no flag. Read the report (stdout or `_reports/<stamp>-dream.md`).
2. Run `--apply-safe` to take the safe additive wins: orphan index lines get 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 added, profile-only memories get migrated into the repo (conflicts skipped
and reported). and reported).
3. Work the `## PROPOSED` section by hand: 3. Work the `## PROPOSED` section by hand:
- `[MERGE?]` -- decide whether to consolidate a cluster. If yes, author a new - `[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 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 originals via `git rm`, update `MEMORY.md`. Deletions are now first-class
`sync-memory.sh` mirror mode will propagate them to every profile store `sync-memory.sh` mirror mode will propagate them to every profile store
on the next run. on the next run.
- `[REVERIFY?]` -- confirm the dated fact still holds; update the body and - `[REVERIFY?]` -- confirm the dated fact still holds; update the body and
its date if it changed. its date if it changed.
- `[STALE-REF?]` -- confirm the referenced path moved/renamed; repoint or - `[STALE-REF?]` -- confirm the referenced path moved/renamed; repoint or
annotate. Many are legitimately server-side (`.service` units, `/opt/...`). annotate. Many are legitimately server-side (`.service` units, `/opt/...`).
- `[INDEX-CLEANUP?]` / `[DRIFT-RESOLVE?]` -- human picks the winner. - `[INDEX-CLEANUP?]` / `[DRIFT-RESOLVE?]` -- human picks the winner.
4. Commit the repo store changes so they sync to the fleet via Gitea. 4. Commit the repo store changes so they sync to the fleet via Gitea.
## Self-test ## Self-test
`scripts/selftest.py` runs the analyzer against a synthetic fixture memory `scripts/selftest.py` runs the analyzer against a synthetic fixture memory
store in a temp dir and asserts each detector fires (orphan, missing target, store in a temp dir and asserts each detector fires (orphan, missing target,
broken backlink, stale path, cluster, profile drift) and that `--apply-safe` broken backlink, stale path, cluster, profile drift) and that `--apply-safe`
only touches the things it's supposed to (index appends + profile→repo copy 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). of new files; no deletions, no merges, no overwrites of differing content).
Run: Run:
```bash ```bash
py "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/selftest.py" bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/memory-dream/scripts/selftest.py"
``` ```

View File

@@ -1,115 +1,115 @@
--- ---
name: packetdial 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." 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 # PacketDial / NetSapiens (OITVOIP) Skill
Standalone CLI client for the NetSapiens SNAPsolution **API v2** that backs Standalone CLI client for the NetSapiens SNAPsolution **API v2** that backs
ACG's hosted-VoIP offering through OITVOIP / PacketDial. Read-only by default; ACG's hosted-VoIP offering through OITVOIP / PacketDial. Read-only by default;
every write (create / update / delete) is gated behind `--confirm`. every write (create / update / delete) is gated behind `--confirm`.
## The two hostnames (important) ## The two hostnames (important)
| Host | What it is | API? | | 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** | | `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 | | `pbx.packetdial.com` | Reseller PBX platform — NetSapiens v44.4. | **Yes** — this skill targets it |
- API base: `https://pbx.packetdial.com/ns-api/v2` - API base: `https://pbx.packetdial.com/ns-api/v2`
- Token endpoint: `https://pbx.packetdial.com/ns-api/v2/tokens` - Token endpoint: `https://pbx.packetdial.com/ns-api/v2/tokens`
- Live OpenAPI spec: `https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json` - Live OpenAPI spec: `https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json`
- Live Swagger UI: `https://pbx.packetdial.com/ns-api/openapi` - 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 - Vendor docs: https://docs.ns-api.com/ (login) and https://voipdocs.io/oitvoip-access-platform-apis
## Credentials — ONE-TIME SETUP (not yet provisioned) ## Credentials — ONE-TIME SETUP (not yet provisioned)
As of this skill's creation **no API key exists yet** — the vault entry 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 `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: clear "No credentials found" error until you do this once:
1. Log into `pbx.packetdial.com` -> **Admin > API Keys** and create a 1. Log into `pbx.packetdial.com` -> **Admin > API Keys** and create a
reseller-scoped key (prefix `nsr_`). If self-service key creation is not reseller-scoped key (prefix `nsr_`). If self-service key creation is not
available, reply to **Darwin Escaro (OITVOIP)** for reseller OAuth client available, reply to **Darwin Escaro (OITVOIP)** for reseller OAuth client
credentials. credentials.
2. Store it in the SOPS vault. Preferred (static bearer key): 2. Store it in the SOPS vault. Preferred (static bearer key):
``` ```
# msp-tools/oitvoip.sops.yaml # msp-tools/oitvoip.sops.yaml
credentials: credentials:
api_key: nsr_xxxxxxxxxxxxxxxx api_key: nsr_xxxxxxxxxxxxxxxx
``` ```
Or, for OAuth2 password-grant credentials: Or, for OAuth2 password-grant credentials:
``` ```
credentials: credentials:
client_id: <client id> client_id: <client id>
client_secret: <client secret> client_secret: <client secret>
username: <portal user@domain> username: <portal user@domain>
password: <portal password> password: <portal password>
``` ```
3. That's it — the client auto-detects which shape is present. 3. That's it — the client auto-detects which shape is present.
The client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env The client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env
-> `PACKETDIAL_CLIENT_ID`+friends env -> vault `credentials.api_key` -> vault -> `PACKETDIAL_CLIENT_ID`+friends env -> vault `credentials.api_key` -> vault
OAuth fields. Env overrides exist for quick testing without touching the vault. OAuth fields. Env overrides exist for quick testing without touching the vault.
## Running the CLI ## Running the CLI
This machine's Python launcher is `py` (per identity.json); `python` / `python3` This machine's Python launcher is `py` (per identity.json); `python` / `python3`
also work. Run from the scripts dir so the two modules resolve. also work. Run from the scripts dir so the two modules resolve.
```bash ```bash
cd C:/claudetools/.claude/skills/packetdial/scripts cd C:/claudetools/.claude/skills/packetdial/scripts
py ns.py status # API version + authenticated key identity bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py status # API version + authenticated key identity
py ns.py domains # list all domains bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domains # list all domains
py ns.py domain <domain> # one domain's config bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domain <domain> # one domain's config
py ns.py users <domain> # users / extensions in a domain bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py users <domain> # users / extensions in a domain
py ns.py user <domain> <user> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py user <domain> <user>
py ns.py phones <domain> # SIP devices registered in a domain bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py phones <domain> # SIP devices registered in a domain
py ns.py dids <domain> # phone numbers (DIDs) on a domain bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dids <domain> # phone numbers (DIDs) on a domain
py ns.py devices <domain> <user> bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py devices <domain> <user>
py ns.py cdrs --domain <domain> --start 2026-06-01 --end 2026-06-02 bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain <domain> --start 2026-06-01 --end 2026-06-02
py ns.py resellers bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py resellers
``` ```
## Writes (gated) ## Writes (gated)
Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you 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. pass `--confirm`. Bodies are raw JSON matching the NetSapiens v2 schema.
```bash ```bash
py ns.py create-domain --body '{"domain":"acme","description":"Acme Inc"}' --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" 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 bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-user acme --body '{"user":"101","name-first-name":"Jane"}' --confirm
py ns.py create-phone acme --body '{...}' --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-phone acme --body '{...}' --confirm
py ns.py create-did acme --body '{"phonenumber":"15205551234"}' --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-did acme --body '{"phonenumber":"15205551234"}' --confirm
py ns.py update-user acme 101 --body '{"name-last-name":"Doe"}' --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py update-user acme 101 --body '{"name-last-name":"Doe"}' --confirm
py ns.py delete-user acme 101 --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py delete-user acme 101 --confirm
``` ```
## Raw escape hatch (any of the 239 v2 paths) ## Raw escape hatch (any of the 239 v2 paths)
The named commands cover the common surface; for anything else, hit the path The named commands cover the common surface; for anything else, hit the path
directly. Non-GET methods still require `--confirm`. directly. Non-GET methods still require `--confirm`.
```bash ```bash
py ns.py raw GET domains/acme/users/101/answerrules bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET domains/acme/users/101/answerrules
py ns.py raw POST domains/acme/users --body '{...}' --confirm bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw POST domains/acme/users --body '{...}' --confirm
``` ```
## Standard provisioning flow (new customer) ## Standard provisioning flow (new customer)
1. `create-domain` -> dial plan auto-generates 1. `create-domain` -> dial plan auto-generates
2. `create-user` per extension 2. `create-user` per extension
3. `create-phone` per SIP device (MAC-provisioned) 3. `create-phone` per SIP device (MAC-provisioned)
4. `create-did` to attach DIDs and route them to users 4. `create-did` to attach DIDs and route them to users
5. Log the work back to the Syncro ticket 5. Log the work back to the Syncro ticket
## Notes ## Notes
- This is the LIVE production reseller PBX. A bad `create-domain` or - This is the LIVE production reseller PBX. A bad `create-domain` or
`delete-user` affects real customers — confirm the target domain first with a `delete-user` affects real customers — confirm the target domain first with a
read command before any write. read command before any write.
- CDR queries can be large; always pass `--start`/`--end` and a `--limit`. - CDR queries can be large; always pass `--start`/`--end` and a `--limit`.
- Reference detail (auth shapes, full endpoint inventory) lives in - Reference detail (auth shapes, full endpoint inventory) lives in
`references/api.md`. `references/api.md`.

View File

@@ -9,7 +9,7 @@ Format: `YYYY-MM-DD | MACHINE | command/skill | error (brief)`
<!-- Append entries below this line --> <!-- Append entries below this line -->
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. 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.