Files
claudetools/session-logs/2026-06-02-session.md
Howard Enos a420b06d8c sync: auto-sync from HOWARD-HOME at 2026-06-02 15:07:39
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-02 15:07:39
2026-06-02 15:07:49 -07:00

265 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session Log — 2026-06-02
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Cleaned and deduplicated an Outlook M365 contacts CSV export for personal use. The source file (`C:\claudetools\.claude\tmp\treb-data\defaultwhat.csv`) was a 92-column Outlook export containing 664 contacts (4,397 physical lines due to multi-line Notes fields). The goal was to produce an importable CSV with duplicates merged so it would re-import into Outlook cleanly.
Initial analysis found the data was cleaner than expected at the row level (0 cross-row shared emails), but the real duplication was *within* each contact: the three email slots (Email / Email 2 / Email 3) held the same address repeated as case-variants and trailing-dot variants (e.g. `tellerbie@afatucson105.org`, `Tellerbie@Afatucson105.Org`, `tellerbie@afatucson105.org.`). Outlook's Address Book renders one line per email address, so one contact appeared as 2-3 "duplicate" lines. A second corruption pattern was found later: truncated-domain variants (`email.arizona.edu` -> `email.edu`) and malformed `name(email)` forms. A Python script (`clean_contacts.py`) was built iteratively to normalize and dedupe email slots, condense triplicated Notes blocks (fuzzy-match, keep most complete copy), merge empty stub contacts, fix display names to show the person's name instead of the raw email, and apply curated same-person/household merges.
Significant time was spent diagnosing why imports failed. First import produced only a few contacts (multi-line Notes broke the legacy line-based importer). Removing the UTF-8 BOM and switching to Windows ANSI (cp1252) encoding with flattened single-line records (Notes newlines -> ` | `) and minimal quoting finally matched the user's working template and imported successfully. A single-contact test file (`alan-test.csv`) confirmed the format.
The latter half of the session was spent clarifying that the *remaining* duplicates the user saw in the Address Book were **import-accumulation artifacts** — Outlook adds a fresh copy on every import and never replaces, and the file had been imported 5+ times during troubleshooting. Different import versions handled Notes differently, so duplicate copies of the same contact had inconsistent notes (one with the note, one blank). The CSV itself was proven clean (0 shared names, 0 shared emails). The user had also been conflating two different people — **Alan Lafever** (gmail, no note) and **Alan Levene** (aol + gmail, with the Phoenix/Tucson household note). Both were verified correct in the clean file. Session ended with the user confirming everything was correct.
Final output: `defaultwhat-clean.csv` — 627 deduped contacts (from 664), cp1252/no-BOM/single-line format ready for a clean wipe-and-import-once.
## Key Decisions
- **Within-contact email dedup over row dedup.** The actual duplication was redundant email slots inside single contacts, not duplicate rows. Normalized by lowercasing, stripping internal spaces and trailing dots; later extended to collapse truncated-domain variants (same local part + domain-label-subset) and extract emails from malformed `name(email)` strings.
- **Kept genuinely distinct multiple emails as real Email 2/Email 3 fields** (Option A), not collapsed to one. The user confirmed this matches Outlook's documented 3-email-per-contact behavior; the multi-line Address Book display is expected rendering, not duplication.
- **cp1252 + no BOM + flattened single-line Notes** for the output. The legacy line-based importer corrupted multi-line quoted Notes and the UTF-8 BOM mangled the first header (`Title`), causing a blank import. Matched the user's `Sample CSV file for importing contacts.csv` template byte-format.
- **Haney split into two people** (Tommy D. Haney with 3 addresses + Sandy Haney), cross-linked via the Spouse field and a shared condensed note — based on note content showing tdhmgtllc/tdhassoc are Tom's businesses and schaney@att.net is Sandy.
- **Conservative auto-merge with curated list.** Fuzzy name matching over-flagged people sharing first names (9 different "Linda"s, multiple "Ron"/"Chris"/"Kevin"). Auto-merged only high-confidence same-person/household pairs (17) via an explicit email-keyed `MERGE_GROUPS` list; left coincidental-first-name matches separate.
- **Deleted junk empty stubs** (FAQ, Owner, Undisclosed-Recipient:;, Linda, Brad, Brady) but kept name-only real-person stubs (Arlene Lombard, Vanna Randall, Brian/Sze Miller).
## Problems Encountered
- **Heredoc / `python3` failed on Windows (exit 49).** Switched to writing `clean_contacts.py` to disk and running with `python`.
- **First import: only a few contacts.** Cause: multi-line Notes broke the legacy line-based importer. Fix: flatten Notes newlines to ` | ` so each contact is one physical line.
- **Second import: blank address book.** Cause: UTF-8 BOM corrupted the first header so no fields mapped; cp1252 bytes were also invalid if read as UTF-8. Fix: write no-BOM cp1252, matching the template.
- **Persistent "duplicate" contacts after fixes.** Root cause was Outlook import-accumulation (5+ imports stacking copies across folders), not the CSV. Resolved by explaining the wipe-all-folders-then-import-once procedure; CSV proven dup-free.
- **User/Claude name confusion (Lafever vs Levene).** Two different Alans. The household note ("Linda cell / Alan cell") belongs to Alan Levene; Alan Lafever genuinely has no note. Notes on the duplicate Lafever copies were bleed-over from neighbor William Lafferty during a broken multiline parse. Both verified correct in the clean file.
## Configuration Changes
No repo configuration changed. Working files created in `C:\claudetools\.claude\tmp\treb-data\` (gitignored tmp area):
- `clean_contacts.py` — the deduplication/normalization script (created, edited iteratively)
- `find_dupes.py` — read-only comprehensive duplicate scanner
- `defaultwhat-clean.csv` — final output, 627 contacts
- `alan-test.csv` — single-contact import test file
- Source: `defaultwhat.csv` (user-provided Outlook export, 664 contacts)
## Credentials & Secrets
None. No credentials accessed or created this session.
## Infrastructure & Servers
None touched. Personal/local file task only.
## Commands & Outputs
- `python clean_contacts.py` — final run output: `input contacts: 664 | output contacts: 627`; 152 redundant emails removed across 114 contacts; 49 contacts' notes condensed; 17 curated merges; 6 stub-dup removals; 6 junk-stub deletions.
- Output format verified: no BOM (first bytes `Title`), cp1252, 628 physical lines (1 header + 627 contacts), 92 columns.
## Pending / Incomplete Tasks
- **User must perform the final clean import** in Outlook: delete all contacts in every folder (main Contacts + `Contacts treb737@earthlink.net`), then import `defaultwhat-clean.csv` once with "Do not import duplicate items." The CSV work is complete; remaining duplicates are import-accumulation that only an Outlook wipe clears.
- Working scripts (`clean_contacts.py`, `find_dupes.py`, `alan-test.csv`) left in tmp in case another pass is wanted; can be deleted.
## Reference Information
- Working dir: `C:\claudetools\.claude\tmp\treb-data\`
- Final deliverable: `C:\claudetools\.claude\tmp\treb-data\defaultwhat-clean.csv` (627 contacts)
- Import template reference: `C:\Users\Howard\Downloads\Sample CSV file for importing contacts.csv` (92-col Outlook format, no BOM, ASCII)
- Outlook target address book: `Contacts - treb737@earthlink.net`
- Dedup summary: 664 -> 627; 0 contacts share a name or email in the final file.
---
## Update: 07:24 PDT — Sonnet wiki recompile test + Linux agent drift investigation
### Session Summary
Ran the first live test of the Sonnet-subagent wiki recompile (the engine swap from Ollama qwen3:8b done the prior session) against a real article: `peaceful-spirit`, chosen because it was stale (2026-05-24) and had genuinely new source material (the recovered RADIUS log + cross-linked manual log). The Sonnet subagent produced a high-quality full recompile — preserved all existing Patterns/History verbatim, added 3 well-sourced Patterns and 2 History rows, and absorbed the RADIUS/NPS and 2026-05-27 BridgettePSHomeComputer work. It also correctly extracted the real Syncro customer ID (278525, "Peaceful Spirit Massage") from a session log even though my API name-search for "peaceful spirit" returned only unrelated businesses.
The main-agent review step caught two issues before write: (1) the subagent inlined three raw secrets (sysadmin password, UCG root password, NPS shared secret) into the article — stripped back to vault-references; (2) the Syncro ID was verified against the live API rather than trusted blind (it checked out). Committed the recompile (dc2c754) and then hardened the synthesis brief with rule 6b — never inline raw secrets, vault-reference only (5189f28).
Investigated the GuruRMM Linux-agent fleet drift that GURU-KALI flagged (every pre-30da053 Linux agent supposedly running new binary on an old strict-sandbox systemd unit -> BUG-016 mint+lose device_id on restart). Verified all four Linux agents (all on 0.6.52): gururmm (172.16.3.30, ProtectSystem=false permissive unit, .device-id persists since Apr 16), Jupiter (172.16.3.20, Unraid non-systemd host, no /var/lib/gururmm), ix (172.16.3.10, NO_SANDBOX_DIRECTIVES, .device-id persists since May 28), and GURU-KALI (already remediated 2026-06-01). NONE carry the strict-sandbox unit — GURU-KALI was the only affected host, so the "fleet-wide" concern is disproven and there is no remediation script to run.
The investigation surfaced a real bug instead: GuruRMM Linux agent remote-command execution stalls or fails. Commands dispatched to online Linux agents (Jupiter, ix) either sat in status=running for 5+ minutes with no completion or returned status=failed with empty output, while Windows agents execute fine. This is why the GURU-KALI unit fix had to be applied by hand over SSH rather than pushed via /rmm. Filed as a todo.
### Key Decisions
- Verified the Sonnet subagent path end-to-end rather than trust the prior session's Claude-direct test; the review step proved necessary (caught secret leakage) and stays.
- Hardened the prompt (rule 6b) so secret leakage is prevented at the source, not just caught in review.
- Verified the Linux drift per-host with live evidence rather than accept GURU-KALI's blanket framing; reached ix via paramiko + the vault root password after RMM command + root-key SSH both failed.
- Closed the ix-verify todo as done (verified, no refresh needed); kept the command-exec bug open as the real follow-up.
### Problems Encountered
- Sonnet draft inlined raw secrets from session logs into the wiki — caught in review, stripped, and prevented going forward via rule 6b.
- Syncro name-search for "peaceful spirit" matched unrelated businesses (the Syncro business name is "Peaceful Spirit Massage"); the real ID came from the session log and was API-verified.
- GuruRMM Linux remote-command execution non-functional (stalls/fails) — blocked RMM-based verification of Jupiter/ix; worked around with direct SSH/paramiko. Filed as a bug.
- ix root SSH refused key auth; used the vault root password via paramiko (BatchMode had masked the password-auth path on the first attempt).
### Configuration Changes
- [modified, committed dc2c754] `wiki/clients/peaceful-spirit.md` (full Sonnet recompile) + `wiki/index.md`
- [modified, committed 5189f28] `.claude/commands/wiki-compile.md` (added rule 6b: never inline raw secrets)
- No other repo changes — the Linux investigation was read-only (live queries + SSH).
### Infrastructure & Servers
- GuruRMM Linux agents (all v0.6.52): gururmm 172.16.3.30 (Ubuntu 22.04), Jupiter 172.16.3.20 (Debian/Unraid host), ix.azcomputerguru.com 172.16.3.10 (Rocky/CloudLinux WHM/cPanel, ext 72.194.62.5, WHM 2087 / cPanel 2083), GURU-KALI (Kali, offline).
- Fleet totals: 93 agents (87 Windows, 4 Linux, 2 macOS).
- ix SSH: root@172.16.3.10:22, password in vault `infrastructure/ix-server.sops.yaml`.
- Peaceful Spirit: Syncro customer 278525 ("Peaceful Spirit Massage"), ticket #32271.
### Commands & Outputs
- RMM auth + agent list: `POST http://172.16.3.30:3001/api/auth/login` -> token; `GET /api/agents`.
- Per-host unit check: `systemctl cat gururmm-agent | grep -iE "StateDirectory|ProtectSystem|ReadWritePaths"; ls -l /var/lib/gururmm/.device-id; journalctl -u gururmm-agent --since -7days | grep -ci "persist device ID|EROFS|read-only"`.
- ix reached via paramiko (root + vault password) after `ssh -o BatchMode=yes root@172.16.3.10` returned "Permission denied (publickey,...)".
- RMM command dispatch to Jupiter/ix returned status=running (never completed) / status=failed (empty output).
### Pending / Incomplete Tasks
- **OPEN todo 57e142aa** (gururmm): Linux agent remote-command execution stalls/fails — investigate command_type handling + WS command channel.
- ix-verify todo ad7bbc61 — CLOSED (not at risk).
- Optional: recompile `wiki/projects/gururmm.md` to capture the command-exec bug + the verified "device-id drift was KALI-only" finding.
- Loose end: GURU-KALI flagged the fleet drift in the coord record as an open concern; a coord reply documenting "all Linux hosts verified clean, no script needed" would close that loop (offered, not yet sent).
### Reference Information
- Commits: dc2c754 (peaceful-spirit Sonnet recompile), 5189f28 (wiki-compile rule 6b)
- Todos: 57e142aa (Linux cmd-exec bug, open), ad7bbc61 (ix verify, done)
- GuruRMM API: http://172.16.3.30:3001 (admin@azcomputerguru.com)
- Linux agent IDs: Jupiter 443bfabb-9213-4157-8be6-2b6d5d3113b2, ix 4ad2e426-b03f-4c5d-817c-c8c675ba73a0
- BUG-016 upstream fix: gururmm commit 30da053 (OnceLock device_id + StateDirectory=gururmm)
---
## Update: 10:27 MST — retrieve Claude.ai sign-in link from Mike's mailbox
### Session Summary
Fetched a Claude.ai sign-in link from Mike's M365 mailbox via the /mailbox skill (read `--as mike@azcomputerguru.com`). Reading worked. The Claude.ai login email is a magic **link** (not a numeric code) and is INKY/SafeLinks-wrapped by GuruProtect. Howard requested a fresh link; polled Mike's inbox, caught the newest (received 2026-06-02 17:08 UTC), extracted the wrapped sign-in URL, and delivered it directly. Howard confirmed it worked (signed into Mike's Claude.ai).
Attempted to forward the email to howard@azcomputerguru.com via Graph `/messages/{id}/forward` -> **403 ErrorAccessDenied**. The Claude-MSP-Access app (`fabb3421`) can READ mailboxes but Mail.Send is not effective tenant-wide, so forward/send/reply are currently broken (reads fine). Pivoted to handing over the link text directly.
### Key Decisions
- Delivered the sign-in link directly in chat rather than via email forward, after the forward 403'd. Link is time-sensitive (~15 min).
- Did not retry the send via sendMail (same Mail.Send perm; skill hard-rule = don't retry ambiguous sends).
### Problems Encountered
- `fabb3421` (Claude-MSP-Access Graph app): Mail.Read works, **Mail.Send returns 403** tenant-wide -> /mailbox send/reply/forward broken. Ties to security todo 10536f07 (this is the deprecated app whose ClientSecret was exposed; perms reduced / rotation pending).
- Claude.ai login = magic **link**, not a numeric code; initial "fetch the code" found no numeric code (the item is the "Secure link to log in to Claude.ai" email).
- INKY-wrapped link: real claude.ai URL is in the INKY `t=h.<base64url-zlib>` token; couldn't cleanly decode, but the full SafeLinks->INKY wrapped URL redirects correctly when opened (usable as-is).
### Configuration Changes (this update)
- None persistent (.claude/tmp/mailbox-token.json token cache only; gitignored).
### Credentials & Secrets (this update)
- Claude-MSP-Access Graph app: client_id `fabb3421-8b34-484b-bc17-e46de9703418`, secret in vault `msp-tools/claude-msp-access-graph-api.sops.yaml` -> `credentials.credential`. NOTE: Mail.Send currently DENIED (403) for this app.
### Pending / Incomplete (this update)
- `fabb3421` Mail.Send broken (403). Restore send perms or (better) finish security todo 10536f07 (rotate/revoke exposed secret + confirm app retirement); move /mailbox to a non-deprecated app if sending is needed.
### Reference (this update)
- M365 tenant azcomputerguru.com (ce61461e-81a0-4c84-bb4a-7b354a9a356d); GuruProtect/INKY (shared.outlook.inky.com) behind Outlook SafeLinks (nam11.safelinks.protection.outlook.com).
- /mailbox skill; vault msp-tools/claude-msp-access-graph-api.sops.yaml; related security todo 10536f07.
## Update: 17:58 PT — Unraid bzfirmware checksum boot failure (infra)
### Session Summary
Howard brought in an Unraid server (Dell-monitored box, Generic 8GB USB flash boot) that halts at boot with `bzfirmware checksum error - press ENTER key to reboot...`. Reviewed a photo of the console (Image #5). Diagnosed the failure: Unraid verifies each boot file (bzimage, bzroot, bzroot-gui, bzmodules, bzfirmware) against a stored SHA256 before mounting the OS. bzimage/bzroot/bzroot-gui/bzmodules all passed; only `bzfirmware` failed its checksum, so the OS never loads and the box reboots in a loop.
Confirmed the flash drive itself is healthy at the filesystem layer: console shows `/dev/sda1` detected with label UNRAID, and `fsck.fat 4.2` ran clean (758 files, no FAT errors). The corruption is isolated to the bytes of the `bzfirmware` file, not the FAT structure. Pressing ENTER only reboots into the same error — it does not self-heal.
Provided the remediation procedure: power off, pull USB, back up the entire stick on a Windows PC (critical: the `config/` folder holding super.dat disk assignments, the *.key license, shares, and network settings), download the exact matching Unraid version zip from unraid.net, then overwrite only the `bz*` files (+ their `.sha256`) on the USB while leaving `config/` untouched. Boot should then verify and the array should come up with existing assignments intact.
Flagged that a single corrupt file on a cheap 8GB generic stick is a common early sign of a failing flash drive (the #1 wear item on Unraid). If the error recurs, plan a USB migration to a quality stick (new GUID requires a free self-service license transfer/replacement key at unraid.net, allowed once per 12 months).
### Key Decisions
- Replace all `bz*` OS files rather than just `bzfirmware`, to guarantee version consistency and avoid kernel/module/firmware mismatch.
- Preserve the existing `config/` folder verbatim — only the OS files are refreshed, so disk assignments and license survive without a license transfer.
- Match the downloaded zip to the exact installed Unraid version before proceeding (asked Howard to confirm server/client/version so we grab the right release).
### Problems Encountered
- bzfirmware checksum mismatch on the Unraid boot USB → fix by overwriting the corrupt `bz*` files from a matching-version Unraid zip; back up `config/` first.
### Configuration Changes
- `.claude/current-mode` set to `infra`.
### Pending / Incomplete Tasks
- Confirm which client/server this Unraid box belongs to and the exact installed Unraid version, then download the matching zip and refresh the `bz*` files.
- Watch for recurrence — if bzfirmware corrupts again, migrate to a new USB and process a license transfer.
### Reference Information
- Boot device: /dev/sda1, label UNRAID, Generic Flash Disk 8GB (8.05 GB / 7.50 GiB).
- Unraid downloads: https://unraid.net (current + previous releases).
- Files to refresh on USB: bzimage, bzroot, bzroot-gui, bzmodules, bzfirmware (+ matching .sha256 each). Do NOT touch config/.
---
## Update: 15:06 PT — PacketDial / OITVOIP API access + new `packetdial` skill
### Session Summary
Howard asked whether Claude has API access to https://voip.packetdial.com/ — specifically whether it can pull data and make changes — and to build a skill if not. Investigation established the answer is "not yet, for lack of credentials," and produced a complete, ready-to-use skill that will work the moment a key is provisioned.
Searched the repo and vault for prior PacketDial context. Found that `packetdial.com` appears throughout as a WordPress/email hosting client (IX web hosting, Neptune Exchange accepted domain) — unrelated to VoIP. The relevant prior work was in `session-logs/2026-04-20-session.md` ("OITVOIP / NetSapiens API Research"), which identified the platform but left credential provisioning as an open TODO. The vault has no `packetdial`/`oitvoip` entry (`vault.sh search` returned nothing for either term).
Probed both PacketDial hostnames live to resolve Mike's questions factually. `voip.packetdial.com` is the customer-facing white-label portal (login-gated UI; the Cascades fax/UC dashboard, account 28598) and exposes no API — `/ns-api/*` there redirects to `errors/not_found`. `pbx.packetdial.com` is the real API host: NetSapiens SNAPsolution API v2, version 44.4.10, a 239-path REST surface with Bearer auth. Pulled the live OpenAPI spec and inventoried the endpoint groups (domains, users, devices/phones, phonenumbers/DIDs, resellers, cdrs, subscriptions, etc.) and the token endpoint shape.
Built the `packetdial` skill modeled on the existing `bitdefender`/`b2` skills: read-by-default, every write gated behind `--confirm`, credentials loaded from the SOPS vault (never hardcoded). Verified both Python modules compile, the write-gate refuses without `--confirm`, and the no-credentials path emits actionable setup guidance rather than a stack trace.
Howard does not have portal access and said Mike has questions about Claude's claims, so the decision was kicked to Mike via a coord todo + broadcast message. Nothing was committed and nothing touches the live PBX.
### Key Decisions
- Targeted `pbx.packetdial.com` (the API host) not `voip.packetdial.com` (portal) — confirmed by live probing, not assumption.
- Skill supports two credential shapes (static `nsr_` bearer key OR OAuth2 password grant), auto-detecting whichever is present in the vault; env-var overrides exist for testing.
- Read-by-default + `--confirm`-gated writes, matching the safety posture of the bitdefender/b2 skills, because this targets the LIVE production reseller PBX affecting real customers.
- Included a `raw` escape hatch so the named commands don't have to cover all 239 paths.
- Did NOT create the vault entry or commit the skill — provisioning a key and approving the skill are Mike's call (Howard lacks portal access).
### Problems Encountered
- Bash tool working directory drifted: an earlier `cd .claude/skills/packetdial/scripts` persisted across calls, so later relative paths (`.claude/scripts/whoami-block.sh`) 404'd. Resolved by `cd C:/claudetools` (absolute) before the save steps.
- `python3 json.load` choked on the 2.7 MB OpenAPI spec under Windows cp1252 default encoding (UnicodeDecodeError). Resolved by opening with `encoding='utf-8'`.
### Configuration Changes
- Created `.claude/skills/packetdial/SKILL.md` (front matter, usage, voip-vs-pbx distinction, one-time credential setup, provisioning flow).
- Created `.claude/skills/packetdial/scripts/ns_client.py` (NetSapiens v2 client: vault credential loading, apikey + OAuth2 modes, httpx/urllib transport, read + gated-write methods).
- Created `.claude/skills/packetdial/scripts/ns.py` (CLI: read commands, `--confirm`-gated write commands, `raw` escape hatch).
- Created `.claude/skills/packetdial/references/api.md` (auth shapes, full endpoint inventory, provisioning flow, history).
### Credentials & Secrets
- NONE created or discovered. PacketDial/OITVOIP API key does not exist yet.
- Planned vault entry (per skill + 2026-04-20 TODO): `msp-tools/oitvoip.sops.yaml` with either `credentials.api_key` (preferred, `nsr_` reseller bearer) OR OAuth fields `credentials.{client_id,client_secret,username,password}`.
### Infrastructure & Servers
- API host: `pbx.packetdial.com` — base `https://pbx.packetdial.com/ns-api/v2`; token `https://pbx.packetdial.com/ns-api/v2/tokens`. NetSapiens SNAPsolution v44.4.10, host portal2-phx.ucaas.network.
- Customer portal (no API): `voip.packetdial.com` — Cascades fax/UC dashboard, account 28598.
- Live OpenAPI: `https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json`; Swagger UI: `https://pbx.packetdial.com/ns-api/openapi`.
### Commands & Outputs
- `python3 ns.py domains` → `[ERROR] No PacketDial / NetSapiens credentials found...` (expected — no creds yet; exit 1).
- `python3 ns.py delete-user acme 101` → `[DRY RUN] Would DELETE user: acme/101 ... Refusing ... without --confirm` (exit 2 — gate works).
- `python3 -m py_compile ns.py ns_client.py` → both compile.
- Created coord todo `7a567a23-eb3d-44b1-a0ee-af40722873ae` (assigned_to_user=mike), broadcast message `e89381f8-b0be-48e2-a13c-92c1aea4e293`.
### Pending / Incomplete Tasks
- AWAITING MIKE (todo 7a567a23): (a) do we manage PacketDial via API at all; (b) if yes, create `nsr_` key in pbx.packetdial.com Admin>API Keys (or get OAuth creds from Darwin Escaro / OITVOIP) and store in vault `msp-tools/oitvoip.sops.yaml`; (c) approve committing the `packetdial` skill.
- Skill is built but NOT committed pending Mike's approval (this `/save` will commit it as a session artifact, but go-live still needs the key + Mike's sign-off).
### Reference Information
- Vendor docs: https://docs.ns-api.com/ (login), https://voipdocs.io/oitvoip-access-platform-apis
- Prior research: `session-logs/2026-04-20-session.md` ("OITVOIP / NetSapiens API Research")
- OITVOIP contact: Darwin Escaro
- Coord todo: `7a567a23-eb3d-44b1-a0ee-af40722873ae` | broadcast msg: `e89381f8-b0be-48e2-a13c-92c1aea4e293`