sizmo 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ # Multi-client workflow
2
+
3
+ `sizmo` supports multiple GoHighLevel locations via named profiles. Each profile holds one PIT and one Location ID.
4
+
5
+ ## Setup — one profile per client
6
+
7
+ ```sh
8
+ echo "pit-clientA..." | sizmo config set --profile clientA --loc LOC_A --pit-stdin
9
+ echo "pit-clientB..." | sizmo config set --profile clientB --loc LOC_B --pit-stdin
10
+ echo "pit-clientC..." | sizmo config set --profile clientC --loc LOC_C --pit-stdin
11
+ ```
12
+
13
+ ## Check all profiles
14
+
15
+ ```sh
16
+ sizmo config list
17
+ ```
18
+
19
+ Output (verified format from `lib/cli.mjs`):
20
+
21
+ ```
22
+ * clientA loc LOC_A pit-…AAAA day 12/90
23
+ clientB loc LOC_B pit-…BBBB day 8/90
24
+ clientC loc LOC_C pit-…CCCC day 45/90
25
+ ```
26
+
27
+ `*` marks the default profile (used when `--profile` is not passed).
28
+
29
+ ## Switch default
30
+
31
+ ```sh
32
+ sizmo config use clientB
33
+ ```
34
+
35
+ ## Run a command against a specific client
36
+
37
+ Pass `--profile` to any command:
38
+
39
+ ```sh
40
+ sizmo brief --profile clientA
41
+ sizmo brief --profile clientB
42
+ sizmo receivables --profile clientC --top 10
43
+ ```
44
+
45
+ ## Morning sweep across all clients
46
+
47
+ There is no built-in "run all profiles" command. Use a shell loop:
48
+
49
+ ```sh
50
+ for p in clientA clientB clientC; do
51
+ echo "=== $p ===";
52
+ sizmo brief --profile $p --json;
53
+ done
54
+ ```
55
+
56
+ Or with `--json` piped to a processor:
57
+
58
+ ```sh
59
+ for p in clientA clientB clientC; do
60
+ sizmo snapshot --profile $p --json
61
+ done
62
+ ```
63
+
64
+ ## PIT rotation per client
65
+
66
+ Each PIT expires at 90 days. `sizmo config list` shows `day N/90` for each. Rotate before day 80:
67
+
68
+ ```sh
69
+ echo "pit-newtoken..." | sizmo config set --profile clientA --pit-stdin --created $(date +%Y-%m-%d)
70
+ sizmo auth check --profile clientA
71
+ ```
72
+
73
+ ## JSON output for automation
74
+
75
+ Every command supports `--json --profile <name>`. The envelope includes `"location"` so you can route results by location ID in your processing code:
76
+
77
+ ```json
78
+ {
79
+ "schemaVersion": 1,
80
+ "command": "snapshot",
81
+ "location": "LOC_A",
82
+ "data": { ... },
83
+ "degraded": false,
84
+ "warnings": []
85
+ }
86
+ ```
@@ -0,0 +1,45 @@
1
+ # noshow — no-show recovery
2
+
3
+ ## What it answers
4
+
5
+ "Who no-showed and hasn't been re-booked?" Lists contacts whose appointment status is `no-show` within the lookback window, so you can follow up and get them rescheduled.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo noshow
11
+ sizmo noshow --days 14 # last 14 days
12
+ sizmo noshow --top 20 # show up to 20
13
+ sizmo noshow --json
14
+ sizmo noshow --profile myclient
15
+ ```
16
+
17
+ Flags (verified from `meta` in `commands/noshow.mjs`):
18
+
19
+ | Flag | Type | Default | Description |
20
+ |------|------|---------|-------------|
21
+ | `--days` | int | 30 | Lookback window |
22
+ | `--top` | int | 15 | Max results |
23
+
24
+ ## How it works
25
+
26
+ Fetches all calendars for the location, then fetches events for each calendar within the window. Events with status `no-show` are collected, deduplicated by contact, and sorted by most recent occurrence.
27
+
28
+ **Known limitation:** GHL's `/calendars/events` endpoint does not support cursor-based pagination. If a calendar returns >= 100 events the result may be silently truncated. The CLI emits `degraded: true` with a warning when this threshold is hit. Full fix (date-window splitting) is tracked as a follow-up.
29
+
30
+ ## Sample output shape (example — no live creds in this context)
31
+
32
+ ```
33
+ NO-SHOWS (last 30d)
34
+ 1. Carlo Reyes 3d ago Calendar: Consultation
35
+ 2. Ana Lim 8d ago Calendar: Strategy Call
36
+ 3. Bong Santos 12d ago Calendar: Consultation
37
+ ```
38
+
39
+ *Sample shape only.*
40
+
41
+ ## Notes
42
+
43
+ - The CLI never sends a message or re-books the appointment. Use this list to identify who to contact; the action stays with you.
44
+ - If `degraded: true` appears in `--json` output, at least one calendar was truncated or blocked. The list is incomplete.
45
+ - Contacts may appear once per no-show event. If someone no-showed twice, they may appear twice.
@@ -0,0 +1,47 @@
1
+ # pipeline — pipeline health
2
+
3
+ ## What it answers
4
+
5
+ "How much value is in each stage, and which deals are stuck?" Shows total opportunity value by pipeline stage plus a stuck-deal sweep listing deals that haven't moved in N days.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo pipeline
11
+ sizmo pipeline --stuck-days 14 # flag deals idle for 14+ days
12
+ sizmo pipeline --top 20 # show top 20 stuck deals
13
+ sizmo pipeline --json
14
+ sizmo pipeline --profile myclient
15
+ ```
16
+
17
+ Flags (verified from `meta` in `commands/pipeline.mjs`):
18
+
19
+ | Flag | Type | Default | Description |
20
+ |------|------|---------|-------------|
21
+ | `--stuck-days` | int | 7 | Idle threshold in days |
22
+ | `--top` | int | 100 | Max stuck deals to show |
23
+
24
+ ## How it works
25
+
26
+ Paginates all opportunities to completion before sorting. A deal is "stuck" when its last status change, stage change, or update timestamp is older than `--stuck-days`. The stuck sweep shows value, stage, and idle age per deal.
27
+
28
+ ## Sample output shape (example — no live creds in this context)
29
+
30
+ ```
31
+ PIPELINE VALUE BY STAGE
32
+ Discovery ₱85,000 (4 deals)
33
+ Proposal ₱140,000 (3 deals)
34
+ Closed Won ₱32,000 (2 deals)
35
+
36
+ STUCK DEALS (idle > 7d)
37
+ 1. Maria Santos Proposal ₱45,000 idle 12d
38
+ 2. Juan dela Cruz Discovery ₱22,000 idle 9d
39
+ ```
40
+
41
+ *Sample shape only.*
42
+
43
+ ## Notes
44
+
45
+ - GHL opportunity `monetaryValue` has no currency field — it inherits the pipeline configuration. The CLI renders the raw value; no currency conversion is performed.
46
+ - `--top N` caps the stuck-deal list. All opportunities are fetched before the cap is applied.
47
+ - A deal last touched via `lastStatusChangeAt`, `lastStageChangeAt`, `updatedAt`, `dateUpdated`, or `dateAdded` (in that priority order) — whichever is most recent.
@@ -0,0 +1,45 @@
1
+ # receivables — A/R who owes
2
+
3
+ ## What it answers
4
+
5
+ "Who owes money, how much, and how old is the invoice?" Lists all outstanding invoices (status: sent, overdue, partially paid, payment processing, viewed, due) sorted by age descending.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo receivables
11
+ sizmo receivables --top 30 # show up to 30 invoices
12
+ sizmo receivables --json
13
+ sizmo receivables --profile myclient
14
+ ```
15
+
16
+ Flags (verified from `meta` in `commands/receivables.mjs`):
17
+
18
+ | Flag | Type | Default | Description |
19
+ |------|------|---------|-------------|
20
+ | `--top` | int | 20 | Max rows to display |
21
+
22
+ ## How it works
23
+
24
+ Paginates all invoices to completion (was previously offset-capped at 2000 — that bug is fixed). Filters to unpaid statuses. Per-currency totals are calculated separately — PHP, USD, EUR, GBP are never cross-summed.
25
+
26
+ Unpaid statuses: `sent`, `overdue`, `partially_paid`, `partially paid`, `payment_processing`, `viewed`, `due`.
27
+
28
+ ## Sample output shape (example — no live creds in this context)
29
+
30
+ ```
31
+ A/R — outstanding invoices
32
+ 1. Juan dela Cruz ₱25,000 21d overdue
33
+ 2. Maria Santos ₱18,000 14d sent
34
+ 3. Carlo Reyes ₱8,500 7d viewed
35
+
36
+ TOTAL: ₱51,500 (PHP)
37
+ ```
38
+
39
+ *Sample shape only.*
40
+
41
+ ## Notes
42
+
43
+ - The CLI never sends an invoice or charges a card. To follow up, use GoHighLevel's invoice tools or your approved agent workflow.
44
+ - Per-currency totals are always separated. If your location has both PHP and USD invoices, each currency totals independently.
45
+ - `--top N` caps the display list. All invoices are fetched before the cap is applied.
@@ -0,0 +1,52 @@
1
+ # reconcile — money collected by source
2
+
3
+ ## What it answers
4
+
5
+ "How much was collected, from which payment sources, and are there any flags?" Breaks down successful transactions by payment provider/source within a window. Also surfaces subscriptions and anomaly flags.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo reconcile
11
+ sizmo reconcile --days 7 # last 7 days
12
+ sizmo reconcile --top 30 # show top 30 sources
13
+ sizmo reconcile --json
14
+ sizmo reconcile --profile myclient
15
+ ```
16
+
17
+ Flags (verified from `meta` in `commands/reconcile.mjs`):
18
+
19
+ | Flag | Type | Default | Description |
20
+ |------|------|---------|-------------|
21
+ | `--days` | int | 30 | Window in days |
22
+ | `--top` | int | 20 | Max source rows |
23
+
24
+ ## How it works
25
+
26
+ Paginates both transactions and subscriptions to completion. Groups successful transactions by source (payment provider, entity source type, or charge snapshot provider — whichever is available). Per-currency totals are kept separate. Flags transactions that look anomalous (e.g. unusually large amounts, missing source attribution).
27
+
28
+ Successful statuses: `succeeded`, `success`, `paid`, `completed`, `captured`.
29
+
30
+ ## Sample output shape (example — no live creds in this context)
31
+
32
+ ```
33
+ COLLECTED — last 30d
34
+
35
+ SOURCE PHP COUNT
36
+ stripe ₱120,000 8
37
+ manual ₱45,000 3
38
+ unknown ₱8,000 1
39
+
40
+ TOTAL: ₱173,000 (PHP)
41
+
42
+ RECURRING SUBSCRIPTIONS: 4 active
43
+ ```
44
+
45
+ *Sample shape only.*
46
+
47
+ ## Notes
48
+
49
+ - The CLI never refunds, voids, or charges. Read-only.
50
+ - Per-currency totals are always separate — never cross-summed.
51
+ - Source attribution relies on GHL's `paymentProviderType`, `providerType`, `source`, or `entitySourceType` fields. Transactions without any of these show as `unknown`.
52
+ - `--days` window is applied to transaction date. Transactions outside the window are excluded.
@@ -0,0 +1,55 @@
1
+ # segment — find contacts by criteria
2
+
3
+ ## What it answers
4
+
5
+ "Which contacts match this combination of criteria?" Filters your contact list by tag, phone presence, creation date, or tag absence. Returns a sample of matching contacts and a total count.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo segment --tag "vip"
11
+ sizmo segment --no-phone
12
+ sizmo segment --tag "lead" --created-days 7
13
+ sizmo segment --without-tag "contacted" --has-phone
14
+ sizmo segment --no-tags
15
+ sizmo segment --top 50 --json
16
+ sizmo segment --profile myclient
17
+ ```
18
+
19
+ Flags (verified from `meta` in `commands/segment.mjs`):
20
+
21
+ | Flag | Type | Default | Description |
22
+ |------|------|---------|-------------|
23
+ | `--tag` | str | — | Must have this tag (case-insensitive) |
24
+ | `--without-tag` | str | — | Must NOT have this tag (case-insensitive) |
25
+ | `--no-tags` | bool | false | Contacts with zero tags |
26
+ | `--created-days` | int | — | Created within N days |
27
+ | `--has-phone` | bool | false | Must have phone number |
28
+ | `--no-phone` | bool | false | Must NOT have phone number |
29
+ | `--top` | int | 20 | Max rows to show in sample |
30
+
31
+ Flags can be combined. All criteria are ANDed.
32
+
33
+ ## How it works
34
+
35
+ Paginates all contacts to completion before applying filters. Tag matching is case-insensitive. The result includes a total match count plus a sample capped at `--top`.
36
+
37
+ ## Sample output shape (example — no live creds in this context)
38
+
39
+ ```
40
+ SEGMENT: tag=vip, no-phone
41
+ Total matching: 8
42
+
43
+ 1. Maria Santos (no phone) tags: vip, inquiry
44
+ 2. Juan dela Cruz (no phone) tags: vip
45
+ 3. Carlo Reyes (no phone) tags: vip, paid
46
+ ```
47
+
48
+ *Sample shape only.*
49
+
50
+ ## Notes
51
+
52
+ - The CLI never writes a tag or modifies a contact. Read-only.
53
+ - `--no-tags` and `--tag` are mutually exclusive in practical use — a contact with zero tags cannot also have a specific tag.
54
+ - Pagination exhausts all contacts before filtering. For large locations (10k+ contacts) this may take a few seconds.
55
+ - `--top N` caps the display sample, not the total count. The total count in the output reflects all matching contacts.
@@ -0,0 +1,52 @@
1
+ # snapshot — 6-metric card
2
+
3
+ ## What it answers
4
+
5
+ "How did the last N days look?" One-screen summary: new leads, bookings, show rate, collected revenue (per currency), conversation reply rate, and total pipeline value.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo snapshot
11
+ sizmo snapshot --days 30 # 30-day window instead of 7
12
+ sizmo snapshot --json
13
+ sizmo snapshot --profile myclient
14
+ ```
15
+
16
+ Flags (verified from `meta` in `commands/snapshot.mjs`):
17
+
18
+ | Flag | Type | Default | Description |
19
+ |------|------|---------|-------------|
20
+ | `--days` | int | 7 | Window in days |
21
+
22
+ ## Metrics
23
+
24
+ All six metrics are derived from live API reads. Each can appear as blocked (scope missing) in which case `degraded: true` is set and the value shows `⚠ can't see (reason)`.
25
+
26
+ | Metric | Source |
27
+ |--------|--------|
28
+ | New leads | Contacts created within window |
29
+ | Bookings | Calendar appointments created within window (all calendars) |
30
+ | Show rate | Attended / (Attended + No-show) within window |
31
+ | Collected | Sum of successful payment transactions within window, per currency |
32
+ | Reply rate | Conversations with an outbound message / total conversations with inbound activity |
33
+ | Pipeline | Sum of all open opportunity values across all pipelines |
34
+
35
+ ## Sample output shape (example — no live creds in this context)
36
+
37
+ ```
38
+ New leads 12
39
+ Bookings 8
40
+ Show rate 75%
41
+ Collected ₱48,000
42
+ Reply rate 83%
43
+ Pipeline ₱320,000
44
+ ```
45
+
46
+ *Sample shape only. Actual values depend on your GoHighLevel location data.*
47
+
48
+ ## Notes
49
+
50
+ - Revenue is tracked per currency — never cross-summed. If you have both PHP and USD transactions, each appears on its own line.
51
+ - Pipeline value uses GHL opportunity `monetaryValue`. GHL does not attach a currency field to individual opportunities — they inherit pipeline config. The CLI renders the value without conversion.
52
+ - Show rate requires `calendars.read` scope. If blocked, the metric shows degraded.
@@ -0,0 +1,44 @@
1
+ # triage — who's waiting on a reply
2
+
3
+ ## What it answers
4
+
5
+ "Who has been waiting the longest for a response?" Surfaces conversation threads where the last message was inbound (from the contact) and no outbound reply has been sent, sorted by wait time descending.
6
+
7
+ ## Command
8
+
9
+ ```sh
10
+ sizmo triage
11
+ sizmo triage --top 20 # show top 20 threads
12
+ sizmo triage --days 14 # narrow lookback to 14 days
13
+ sizmo triage --json
14
+ sizmo triage --profile myclient
15
+ ```
16
+
17
+ Flags (verified from `meta` in `commands/triage.mjs`):
18
+
19
+ | Flag | Type | Default | Description |
20
+ |------|------|---------|-------------|
21
+ | `--top` | int | 10 | Max threads to show |
22
+ | `--days` | int | 30 | Lookback window |
23
+
24
+ ## How it works
25
+
26
+ Paginates all conversations to completion (not capped at a single page) before applying `--top`, so the top N results are truly the longest-waiting — not just the first page. Each thread is examined for its last message direction. Threads where the last message was inbound and is older than the lookback threshold surface as waiting.
27
+
28
+ Channel labels: SMS, Email, Call, FB, IG, WhatsApp, GMB, Chat.
29
+
30
+ ## Sample output shape (example — no live creds in this context)
31
+
32
+ ```
33
+ 1. Maria Santos SMS waiting 8d → reply or log
34
+ 2. Juan dela Cruz Email waiting 5d → reply or log
35
+ 3. Carlo Reyes WhatsApp waiting 3d → reply or log
36
+ ```
37
+
38
+ *Sample shape only.*
39
+
40
+ ## Notes
41
+
42
+ - `--top N` caps the final sorted list, not the pagination. All pages are fetched before the cap is applied.
43
+ - The CLI never sends a reply. To act on a thread, use your GoHighLevel inbox or your approved agent workflow.
44
+ - `--days` filters the lookback window for which conversations are considered. Threads older than the window are excluded.
package/lib/cache.mjs ADDED
@@ -0,0 +1,36 @@
1
+ // lib/cache.mjs — honest on-disk TTL cache.
2
+ // Guardrails: short TTL, age always tracked, atomic write (temp+rename), 0700 dir / 0600 files.
3
+ // NEVER stores whether a response is healthy/degraded — callers only cache 2xx (see http.mjs).
4
+ import { createHash } from 'node:crypto';
5
+ import { mkdirSync, writeFileSync, readFileSync, renameSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { tmpdir } from 'node:os';
8
+
9
+ function keyFile(dir, key) {
10
+ const h = createHash('sha256').update(key).digest('hex');
11
+ return join(dir, h + '.json');
12
+ }
13
+
14
+ export function makeCache({ dir, ttlMs, now = Date.now }) {
15
+ return {
16
+ set(key, value) {
17
+ try {
18
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
19
+ const payload = JSON.stringify({ ts: now(), value });
20
+ // atomic: write to tmp then rename
21
+ const tmp = join(tmpdir(), 'ghl-cache-' + createHash('sha256').update(key + now()).digest('hex').slice(0, 16) + '.tmp');
22
+ writeFileSync(tmp, payload, { mode: 0o600 });
23
+ renameSync(tmp, keyFile(dir, key));
24
+ } catch { /* best-effort — cache write failures are non-fatal */ }
25
+ },
26
+ get(key) {
27
+ try {
28
+ const raw = readFileSync(keyFile(dir, key), 'utf8');
29
+ const { ts, value } = JSON.parse(raw);
30
+ const ageMs = now() - ts;
31
+ if (ageMs > ttlMs) return null; // expired
32
+ return { value, ageMs };
33
+ } catch { return null; } // missing or corrupt → null, no throw
34
+ },
35
+ };
36
+ }