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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 <uuid> --print
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" .claude/scripts/recover_session.py --uuid <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
|
||||
```
|
||||
|
||||
31
.claude/scripts/py.sh
Executable file
31
.claude/scripts/py.sh
Executable 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" "$@"
|
||||
@@ -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
|
||||
|
||||
@@ -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 <id> --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 <id> --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
|
||||
|
||||
@@ -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" <command> ...
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py" <command> ...
|
||||
```
|
||||
|
||||
| Command | What it does |
|
||||
|
||||
@@ -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 <api_key>` |
|
||||
| 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 <id>
|
||||
py mp.py domain <domain_id>
|
||||
py mp.py customers <reseller_id>
|
||||
py mp.py customer <customer_id>
|
||||
py mp.py users <scope> <id>
|
||||
py mp.py user <user_id>
|
||||
py 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
|
||||
py mp.py rules <scope> <id>
|
||||
```
|
||||
|
||||
### 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 <domain_id> --recipient ceo@client.com --decision quarantine_spam
|
||||
|
||||
# Held / quarantined mail search.
|
||||
py mp.py messages domains <domain_id> --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 <message_id> --confirm
|
||||
py mp.py release <message_id> --recipients alt@client.com --confirm
|
||||
py mp.py release-many <scope> <id> --ids 111,222,333 --confirm
|
||||
py mp.py release-many <scope> <id> --all --confirm
|
||||
py mp.py add-rule <scope> <id> --value vendor.com --type allow --confirm
|
||||
py mp.py enable-release <scope> <id> --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 <scope> <id>` — check `allow_spam_release`.
|
||||
2. If `false`: `py mp.py enable-release <scope> <id> --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 <customer_id>
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
py mp.py enable-release domains <domain_id> --confirm # only if needed
|
||||
|
||||
# 4. Release by message id (DRY RUN first — omit --confirm to preview).
|
||||
py mp.py release <message_id> # [DRY RUN]
|
||||
py mp.py release <message_id> --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/<id>/logs
|
||||
py mp.py raw POST messages/<id>/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 <api_key>` |
|
||||
| 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 <id>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domain <domain_id>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customers <reseller_id>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customer <customer_id>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py users <scope> <id>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py user <user_id>
|
||||
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 <scope> <id> # shows permissions.messages.allow_spam_release
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py rules <scope> <id>
|
||||
```
|
||||
|
||||
### 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 <domain_id> --recipient ceo@client.com --decision quarantine_spam
|
||||
|
||||
# Held / quarantined mail search.
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains <domain_id> --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 <message_id> --confirm
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --recipients alt@client.com --confirm
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many <scope> <id> --ids 111,222,333 --confirm
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many <scope> <id> --all --confirm
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py add-rule <scope> <id> --value vendor.com --type allow --confirm
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --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 <scope> <id>` — check `allow_spam_release`.
|
||||
2. If `false`: `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --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 <customer_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 <domain_id> --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 <domain_id> # check allow_spam_release
|
||||
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).
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> # [DRY RUN]
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --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/<id>/logs
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw POST messages/<id>/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`.
|
||||
|
||||
@@ -137,7 +137,7 @@ state first with `config <scope> <id>` and look at
|
||||
## 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
|
||||
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 <scope> <id>` — confirm `allow_spam_release` is `false`.
|
||||
2. `py mp.py enable-release <scope> <id> --confirm` — flip it to `true`.
|
||||
1. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config <scope> <id>` — confirm `allow_spam_release` is `false`.
|
||||
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`.
|
||||
|
||||
Virus and policy quarantines are governed separately — only spam release is
|
||||
|
||||
@@ -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/<slug>/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 <date>"
|
||||
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 `## <Type>` 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 <path>` -- 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/<stamp>-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/<stamp>-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/<slug>/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 <date>"
|
||||
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 `## <Type>` 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 <path>` -- 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/<stamp>-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/<stamp>-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"
|
||||
```
|
||||
|
||||
@@ -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 id>
|
||||
client_secret: <client secret>
|
||||
username: <portal user@domain>
|
||||
password: <portal 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 <domain> # one domain's config
|
||||
py ns.py users <domain> # users / extensions in a domain
|
||||
py ns.py user <domain> <user>
|
||||
py ns.py phones <domain> # SIP devices registered in a domain
|
||||
py ns.py dids <domain> # phone numbers (DIDs) on a domain
|
||||
py ns.py devices <domain> <user>
|
||||
py ns.py cdrs --domain <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 id>
|
||||
client_secret: <client secret>
|
||||
username: <portal user@domain>
|
||||
password: <portal 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 <domain> # one domain's config
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py users <domain> # users / extensions in a domain
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py user <domain> <user>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py phones <domain> # SIP devices registered in a domain
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dids <domain> # phone numbers (DIDs) on a domain
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py devices <domain> <user>
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain <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`.
|
||||
|
||||
@@ -9,7 +9,7 @@ Format: `YYYY-MM-DD | MACHINE | command/skill | error (brief)`
|
||||
|
||||
<!-- 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user