optimal-cli 1.0.0 → 1.1.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.
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.env.example +17 -0
- package/CLAUDE.md +67 -0
- package/COMMANDS.md +264 -0
- package/PUBLISH.md +70 -0
- package/agents/content-ops.md +2 -2
- package/agents/financial-ops.md +2 -2
- package/agents/infra-ops.md +2 -2
- package/apps/.gitkeep +0 -0
- package/bin/optimal.ts +278 -591
- package/docs/MIGRATION_NEEDED.md +37 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
- package/hooks/.gitkeep +0 -0
- package/lib/config/registry.ts +5 -4
- package/lib/kanban-obsidian.ts +232 -0
- package/lib/kanban-sync.ts +258 -0
- package/lib/kanban.ts +239 -0
- package/lib/obsidian-tasks.ts +231 -0
- package/package.json +5 -19
- package/pnpm-workspace.yaml +3 -0
- package/scripts/check-table.ts +24 -0
- package/scripts/create-tables.ts +94 -0
- package/scripts/migrate-kanban.sh +28 -0
- package/scripts/migrate-v2.ts +78 -0
- package/scripts/migrate.ts +79 -0
- package/scripts/run-migration.ts +59 -0
- package/scripts/seed-board.ts +203 -0
- package/scripts/test-kanban.ts +21 -0
- package/skills/audit-financials/SKILL.md +33 -0
- package/skills/board-create/SKILL.md +28 -0
- package/skills/board-update/SKILL.md +27 -0
- package/skills/board-view/SKILL.md +27 -0
- package/skills/delete-batch/SKILL.md +77 -0
- package/skills/deploy/SKILL.md +40 -0
- package/skills/diagnose-months/SKILL.md +68 -0
- package/skills/distribute-newsletter/SKILL.md +58 -0
- package/skills/export-budget/SKILL.md +44 -0
- package/skills/export-kpis/SKILL.md +52 -0
- package/skills/generate-netsuite-template/SKILL.md +51 -0
- package/skills/generate-newsletter/SKILL.md +53 -0
- package/skills/generate-newsletter-insurance/SKILL.md +59 -0
- package/skills/generate-social-posts/SKILL.md +67 -0
- package/skills/health-check/SKILL.md +42 -0
- package/skills/ingest-transactions/SKILL.md +51 -0
- package/skills/manage-cms/SKILL.md +50 -0
- package/skills/manage-scenarios/SKILL.md +83 -0
- package/skills/migrate-db/SKILL.md +79 -0
- package/skills/preview-newsletter/SKILL.md +50 -0
- package/skills/project-budget/SKILL.md +60 -0
- package/skills/publish-blog/SKILL.md +70 -0
- package/skills/publish-social-posts/SKILL.md +70 -0
- package/skills/rate-anomalies/SKILL.md +62 -0
- package/skills/scrape-ads/SKILL.md +49 -0
- package/skills/stamp-transactions/SKILL.md +62 -0
- package/skills/upload-income-statements/SKILL.md +54 -0
- package/skills/upload-netsuite/SKILL.md +56 -0
- package/skills/upload-r1/SKILL.md +45 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/.gitkeep +0 -0
- package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
- package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
- package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
- package/tests/config-command-smoke.test.ts +395 -0
- package/tests/config-registry.test.ts +173 -0
- package/tsconfig.json +19 -0
- package/agents/profiles.json +0 -5
- package/docs/CLI-REFERENCE.md +0 -361
- package/lib/assets/index.ts +0 -225
- package/lib/assets.ts +0 -124
- package/lib/auth/index.ts +0 -189
- package/lib/board/index.ts +0 -309
- package/lib/board/types.ts +0 -124
- package/lib/bot/claim.ts +0 -43
- package/lib/bot/coordinator.ts +0 -254
- package/lib/bot/heartbeat.ts +0 -37
- package/lib/bot/index.ts +0 -9
- package/lib/bot/protocol.ts +0 -99
- package/lib/bot/reporter.ts +0 -42
- package/lib/bot/skills.ts +0 -81
- package/lib/errors.ts +0 -129
- package/lib/format.ts +0 -120
- package/lib/returnpro/validate.ts +0 -154
- package/lib/social/meta.ts +0 -228
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: diagnose-months
|
|
3
|
+
description: Find FK resolution failures and data gaps in staged financial months
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Diagnoses data quality issues in `stg_financials_raw` by scanning for foreign key resolution failures (orphaned account codes, unresolved program IDs, missing client mappings) and data gaps (months with unexpectedly low row counts). This is the go-to debugging tool when audit-financials reports accuracy below 100% — it pinpoints exactly which rows failed to resolve against dimension tables.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **months** (optional): Comma-separated YYYY-MM strings to diagnose. Default: all months with data.
|
|
11
|
+
- **verbose** (optional): Show individual failing rows (not just summary counts). Default: false.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
1. Call `lib/returnpro/diagnose.ts::diagnoseMonths(months?, options?)` to run diagnostics
|
|
15
|
+
2. For each target month, query `stg_financials_raw` and attempt FK resolution:
|
|
16
|
+
- Join `account_code` against `dim_account.account_code` — flag unresolved
|
|
17
|
+
- Join `master_program_id` against `dim_master_program.id` — flag unresolved
|
|
18
|
+
- Join `client_id` against `dim_client.id` — flag unresolved (if present)
|
|
19
|
+
3. Count rows per month and compare against expected baseline (~88-93 accounts/month for staging)
|
|
20
|
+
4. Identify months with zero staging data (data gaps)
|
|
21
|
+
5. Summarize failures by type and month
|
|
22
|
+
6. Log execution via `lib/kanban.ts::logSkillExecution()`
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
Summary table:
|
|
26
|
+
|
|
27
|
+
| Month | Rows | Unresolved Accounts | Unresolved Programs | Unresolved Clients | Status |
|
|
28
|
+
|-------|------|---------------------|---------------------|--------------------|--------|
|
|
29
|
+
| 2026-01 | 91 | 0 | 2 | 0 | 2 issues |
|
|
30
|
+
| 2025-12 | 89 | 1 | 0 | 0 | 1 issue |
|
|
31
|
+
| 2025-11 | 0 | - | - | - | NO DATA |
|
|
32
|
+
|
|
33
|
+
With `--verbose`, individual failing rows are listed below the summary:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Unresolved programs in 2026-01:
|
|
37
|
+
Row 45: account_code=4100, master_program_id=999 (no match in dim_master_program)
|
|
38
|
+
Row 72: account_code=5200, master_program_id=1001 (no match in dim_master_program)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI Usage
|
|
42
|
+
```bash
|
|
43
|
+
# Diagnose all months
|
|
44
|
+
optimal diagnose-months
|
|
45
|
+
|
|
46
|
+
# Specific months
|
|
47
|
+
optimal diagnose-months --months 2026-01,2025-12
|
|
48
|
+
|
|
49
|
+
# Verbose output with individual rows
|
|
50
|
+
optimal diagnose-months --months 2026-01 --verbose
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Environment
|
|
54
|
+
Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
|
|
55
|
+
|
|
56
|
+
## Tables Touched
|
|
57
|
+
- `stg_financials_raw` — scan for FK issues
|
|
58
|
+
- `dim_account` — validate account codes
|
|
59
|
+
- `dim_master_program` — validate program IDs
|
|
60
|
+
- `dim_client` — validate client IDs
|
|
61
|
+
- `dim_program_id` — validate program IDs (secondary lookup)
|
|
62
|
+
|
|
63
|
+
## Gotchas
|
|
64
|
+
- **Coverage gap is expected**: Staging has ~88-93 accounts/month vs confirmed's ~185-193. This is because not all GL accounts are in Solution7. Diagnose-months focuses on FK resolution failures within the staged data, not the coverage gap itself.
|
|
65
|
+
- **amount is TEXT**: Remember to CAST when doing any numeric analysis on flagged rows.
|
|
66
|
+
|
|
67
|
+
## Status
|
|
68
|
+
Implementation status: Not yet implemented. Spec only. Lib function `lib/returnpro/diagnose.ts` to be extracted from dashboard-returnpro's `/api/admin/diagnose-months` route.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: distribute-newsletter
|
|
3
|
+
description: Push a published newsletter to GoHighLevel for email distribution via n8n webhook
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Triggers distribution of a published Strapi newsletter to subscribers via GoHighLevel email. Uses n8n as the orchestration layer — the skill fires a webhook that kicks off the n8n distribution workflow, which handles email blast creation, recipient targeting, and delivery tracking with 3x retry logic.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **documentId** (required): Strapi newsletter documentId to distribute.
|
|
11
|
+
- **brand** (optional): Brand filter for recipient targeting (`CRE-11TRUST` or `LIFEINSUR`). Auto-detected from the newsletter if not provided.
|
|
12
|
+
- **test** (optional): Send to a test recipient list instead of the full subscriber list. Useful for previewing the email in an inbox before full blast.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
1. Call `lib/newsletter/distribute.ts::distributeNewsletter(documentId, options?)` to orchestrate
|
|
16
|
+
2. **Fetch newsletter from Strapi** — `strapiGet('/api/newsletters/{documentId}')` — verify it is published (not draft)
|
|
17
|
+
3. **Validate HTML body** — ensure `html_body` is present and non-empty
|
|
18
|
+
4. **Determine brand** — from newsletter's `brand` field or `--brand` override
|
|
19
|
+
5. **Fire n8n webhook** — POST to the n8n distribution webhook URL with payload: `{ documentId, brand, html_body, subject_line, sender_email, test }`
|
|
20
|
+
6. **n8n workflow handles**: GHL campaign creation, recipient targeting by brand, email send, 3x retry on failures
|
|
21
|
+
7. **Poll for delivery status** — check newsletter's `delivery_status` field (pending → sending → delivered/partial/failed) with 10s intervals, timeout after 5 minutes
|
|
22
|
+
8. **Update Strapi** — `strapiPut('/api/newsletters', documentId, { delivery_status, delivered_at, recipients_count })` (done by n8n, but verify here)
|
|
23
|
+
9. Log execution via `lib/kanban.ts::logSkillExecution()`
|
|
24
|
+
|
|
25
|
+
## Output
|
|
26
|
+
```
|
|
27
|
+
Newsletter: "South Florida CRE Market Update — March 2026"
|
|
28
|
+
Brand: CRE-11TRUST
|
|
29
|
+
Delivery status: delivered
|
|
30
|
+
Recipients: 342
|
|
31
|
+
Delivered at: 2026-03-01T09:15:00Z
|
|
32
|
+
GHL Campaign ID: camp_abc123
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## CLI Usage
|
|
36
|
+
```bash
|
|
37
|
+
# Distribute a published newsletter
|
|
38
|
+
optimal distribute-newsletter --documentId abc123-def456
|
|
39
|
+
|
|
40
|
+
# Test send first
|
|
41
|
+
optimal distribute-newsletter --documentId abc123-def456 --test
|
|
42
|
+
|
|
43
|
+
# Explicit brand override
|
|
44
|
+
optimal distribute-newsletter --documentId abc123-def456 --brand LIFEINSUR
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Environment
|
|
48
|
+
Requires: `STRAPI_URL`, `STRAPI_API_TOKEN`, `N8N_WEBHOOK_URL` (distribution trigger endpoint)
|
|
49
|
+
GoHighLevel credentials are stored in n8n, not in the CLI.
|
|
50
|
+
|
|
51
|
+
## Gotchas
|
|
52
|
+
- **Must be published**: The newsletter must be in "published" state in Strapi. Draft newsletters cannot be distributed.
|
|
53
|
+
- **n8n must be running**: The distribution workflow depends on n8n being active (`npx n8n` or running as service on port 5678).
|
|
54
|
+
- **3x retry logic**: n8n handles retries. If all 3 attempts fail, `delivery_status` is set to `failed` and `delivery_errors` JSON contains the failure details.
|
|
55
|
+
- **Scheduling**: Newsletters with a future `scheduled_date` are queued and distributed by a 15-minute n8n cron, not immediately.
|
|
56
|
+
|
|
57
|
+
## Status
|
|
58
|
+
Implementation status: Not yet implemented. Spec only. Lib function `lib/newsletter/distribute.ts` to be built as a webhook trigger client.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: export-budget
|
|
3
|
+
description: Export FY26 budget projections as CSV with unit, retail, and inventory value columns
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Exports FY26 budget projections as a CSV file suitable for import into spreadsheets, Vena, or other planning tools. Includes full unit projection, average retail projection, and computed inventory value (units x retail) columns.
|
|
8
|
+
|
|
9
|
+
This is a convenience wrapper around `project-budget --format csv` that writes directly to stdout for piping to a file.
|
|
10
|
+
|
|
11
|
+
## Inputs
|
|
12
|
+
- **adjustment-type** (optional): `percent` or `flat`. Default: `percent`.
|
|
13
|
+
- **adjustment-value** (optional): Numeric value for the adjustment. Default: `0` (no change).
|
|
14
|
+
- **fiscal-year** (optional): Base fiscal year for actuals. Default: `2025`.
|
|
15
|
+
- **user-id** (optional): Supabase user UUID to filter imports by.
|
|
16
|
+
- **file** (optional): Path to a JSON file containing `CheckedInUnitsSummary[]` array.
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
1. Load FY25 data (same as `project-budget`)
|
|
20
|
+
2. Initialize projections
|
|
21
|
+
3. Apply adjustment if specified
|
|
22
|
+
4. Call `exportToCSV(projections)` and write to stdout
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
CSV with these columns:
|
|
26
|
+
- Program Code, Master Program, Client
|
|
27
|
+
- 2025 Actual Units, Unit Adj Type, Unit Adj Value, 2026 Projected Units, Unit Change, Unit Change %
|
|
28
|
+
- 2025 Avg Retail, Retail Adj Type, Retail Adj Value, 2026 Projected Retail, Retail Change, Retail Change %
|
|
29
|
+
- 2025 Inventory Value, 2026 Projected Inv. Value, Inv. Value Change, Inv. Value Change %
|
|
30
|
+
|
|
31
|
+
## CLI Usage
|
|
32
|
+
```bash
|
|
33
|
+
# Export with 4% growth to file
|
|
34
|
+
npx tsx bin/optimal.ts export-budget --adjustment-type percent --adjustment-value 4 > fy26-projections.csv
|
|
35
|
+
|
|
36
|
+
# Export from JSON data source
|
|
37
|
+
npx tsx bin/optimal.ts export-budget --file ./fy25-actuals.json > fy26-projections.csv
|
|
38
|
+
|
|
39
|
+
# No adjustment (baseline copy)
|
|
40
|
+
npx tsx bin/optimal.ts export-budget > fy26-baseline.csv
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
Requires (when using Supabase): `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: export-kpis
|
|
3
|
+
description: Export KPI totals by program and client from ReturnPro financial data
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Exports KPI data aggregated by program, client, and month from ReturnPro's `stg_financials_raw` table via the `get_kpi_totals_by_program_client` RPC function. Useful for ad-hoc financial analysis, stakeholder reporting, and data validation.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **months** (optional): Comma-separated YYYY-MM strings (e.g., `2026-01,2025-12`). Defaults to the 3 most recent months with data.
|
|
11
|
+
- **programs** (optional): Comma-separated program name substrings for case-insensitive filtering (e.g., `BRTON,FORTX`).
|
|
12
|
+
- **format** (optional): Output format — `table` (markdown, default) or `csv`.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
1. Call `lib/returnpro/kpis.ts::exportKpis(options?)` to fetch KPI data
|
|
16
|
+
2. Resolve months — use provided list or default to 3 most recent months in `stg_financials_raw`
|
|
17
|
+
3. If programs filter given, resolve names to `master_program_id` via `dim_master_program` (partial match)
|
|
18
|
+
4. For each month (x program), call `get_kpi_totals_by_program_client` RPC
|
|
19
|
+
5. Map results to flat `KpiRow[]` (month, kpiName, kpiBucket, programName, clientName, totalAmount)
|
|
20
|
+
6. Format as markdown table or CSV
|
|
21
|
+
|
|
22
|
+
## Output
|
|
23
|
+
Table format (default):
|
|
24
|
+
|
|
25
|
+
| Month | KPI | Bucket | Client | Program | Amount |
|
|
26
|
+
|---------|-----|--------|--------|---------|--------|
|
|
27
|
+
| 2026-01 | Revenue | Actual | Walmart | BRTON-WM | $1.2M |
|
|
28
|
+
|
|
29
|
+
CSV format: standard comma-separated with header row.
|
|
30
|
+
|
|
31
|
+
Amounts use compact notation ($1.2M, $890K) in table mode, full precision in CSV mode.
|
|
32
|
+
|
|
33
|
+
## CLI Usage
|
|
34
|
+
```bash
|
|
35
|
+
# Latest 3 months, table format
|
|
36
|
+
npx tsx bin/optimal.ts export-kpis
|
|
37
|
+
|
|
38
|
+
# Specific month
|
|
39
|
+
npx tsx bin/optimal.ts export-kpis --months 2026-01
|
|
40
|
+
|
|
41
|
+
# Multiple months, CSV output
|
|
42
|
+
npx tsx bin/optimal.ts export-kpis --months 2025-10,2025-11,2025-12 --format csv
|
|
43
|
+
|
|
44
|
+
# Filter to BRTON programs only
|
|
45
|
+
npx tsx bin/optimal.ts export-kpis --months 2026-01 --programs BRTON
|
|
46
|
+
|
|
47
|
+
# Pipe CSV to file
|
|
48
|
+
npx tsx bin/optimal.ts export-kpis --format csv > kpis-export.csv
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Environment
|
|
52
|
+
Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generate-netsuite-template
|
|
3
|
+
description: Generate a blank NetSuite upload template pre-filled with account codes and program mappings
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Creates a blank upload template (XLSX or CSV) pre-populated with valid account codes, account names, and program mappings from ReturnPro's dimension tables. This saves time when preparing NetSuite data for staging upload — instead of manually looking up codes, Carlos gets a ready-to-fill template with all valid FK references.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **month** (required): Target month as YYYY-MM for the template header/period column.
|
|
11
|
+
- **format** (optional): Output format — `xlsx` (default) or `csv`.
|
|
12
|
+
- **output** (optional): File path to save the template. Default: `~/Downloads/returnpro-data/netsuite-template-{YYYY-MM}.{ext}`
|
|
13
|
+
- **programs** (optional): Comma-separated program name substrings to include. Default: all active programs.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
1. Call `lib/returnpro/templates.ts::generateNetsuiteTemplate(month, options?)` to build the template
|
|
17
|
+
2. Fetch all active accounts from `dim_account` (account_code, account_name)
|
|
18
|
+
3. Fetch all active programs from `dim_master_program` (program_id, program_name)
|
|
19
|
+
4. Build template structure: one row per account_code, columns for period, account_code, account_name, amount (blank), master_program_id, program_name
|
|
20
|
+
5. If `--programs` filter given, only include matching programs
|
|
21
|
+
6. Write to disk as XLSX (with header formatting) or CSV
|
|
22
|
+
7. Log execution via `lib/kanban.ts::logSkillExecution()`
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
```
|
|
26
|
+
Generated NetSuite template for 2026-01
|
|
27
|
+
Accounts: 193 | Programs: 97
|
|
28
|
+
Saved to: ~/Downloads/returnpro-data/netsuite-template-2026-01.xlsx
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI Usage
|
|
32
|
+
```bash
|
|
33
|
+
# Default XLSX template
|
|
34
|
+
optimal generate-netsuite-template --month 2026-01
|
|
35
|
+
|
|
36
|
+
# CSV format, custom output path
|
|
37
|
+
optimal generate-netsuite-template --month 2026-01 --format csv --output ./template.csv
|
|
38
|
+
|
|
39
|
+
# Only BRTON programs
|
|
40
|
+
optimal generate-netsuite-template --month 2026-01 --programs BRTON
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
|
|
45
|
+
|
|
46
|
+
## Tables Touched
|
|
47
|
+
- `dim_account` — read account codes and names
|
|
48
|
+
- `dim_master_program` — read program IDs and names
|
|
49
|
+
|
|
50
|
+
## Status
|
|
51
|
+
Implementation status: Not yet implemented. Spec only. Lib function `lib/returnpro/templates.ts` to be extracted from dashboard-returnpro's `/api/admin/netsuite-template` route.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generate-newsletter
|
|
3
|
+
description: Generate a branded newsletter with AI-powered content, news, and optional property listings, then push to Strapi CMS
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
End-to-end newsletter generation pipeline. Fetches news from NewsAPI, generates AI summaries via Groq (Llama 3.3 70B), reads property listings from Excel (CRE brand only), builds branded HTML, and pushes a draft to Strapi CMS. Supports multiple brands: CRE-11TRUST (ElevenTrust) and LIFEINSUR (Anchor Point Insurance).
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **brand** (required): `CRE-11TRUST` or `LIFEINSUR`
|
|
11
|
+
- **date** (optional): Edition date as YYYY-MM-DD (default: today)
|
|
12
|
+
- **excel** (optional): Path to Excel file with property listings (columnar format: col A = labels, B-N = properties). Only used for CRE-11TRUST brand.
|
|
13
|
+
- **dry-run** (optional): If set, generates content but does NOT push to Strapi. Useful for previewing output.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
1. **Load brand config** — determines news query, sender email, display name, template styling
|
|
17
|
+
2. **Read Excel properties** (CRE-11TRUST only) — parse columnar Excel via `readExcelProperties()`
|
|
18
|
+
3. **Fetch news** — `fetchNews(query)` hits NewsAPI for 5 latest articles matching the brand's query
|
|
19
|
+
4. **Generate AI content** — `generateAiContent(properties, news)` sends properties + news to Groq and gets back market overview, property analyses, and news summaries as structured JSON
|
|
20
|
+
5. **Build HTML** — `buildHtml()` assembles a responsive email-safe HTML newsletter with brand-specific colors and sections
|
|
21
|
+
6. **Build Strapi payload** — `buildStrapiPayload()` creates the structured payload with slug (includes timestamp for uniqueness)
|
|
22
|
+
7. **Push to Strapi** — `strapiPost('/api/newsletters', data)` creates a draft newsletter in Strapi CMS (skipped in dry-run mode)
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
- Newsletter HTML (logged length)
|
|
26
|
+
- Strapi draft documentId (or "DRY RUN" indicator)
|
|
27
|
+
- Console summary of all generated content
|
|
28
|
+
|
|
29
|
+
## CLI Usage
|
|
30
|
+
```bash
|
|
31
|
+
# CRE newsletter with properties from Excel
|
|
32
|
+
optimal generate-newsletter --brand CRE-11TRUST --excel ~/projects/newsletter-automation/input/properties.xlsx
|
|
33
|
+
|
|
34
|
+
# LIFEINSUR newsletter (no properties)
|
|
35
|
+
optimal generate-newsletter --brand LIFEINSUR
|
|
36
|
+
|
|
37
|
+
# Preview without pushing to Strapi
|
|
38
|
+
optimal generate-newsletter --brand CRE-11TRUST --dry-run
|
|
39
|
+
|
|
40
|
+
# Specific edition date
|
|
41
|
+
optimal generate-newsletter --brand CRE-11TRUST --date 2026-03-01 --excel ./input/latest.xlsx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Environment
|
|
45
|
+
Requires: `GROQ_API_KEY`, `NEWSAPI_KEY`, `STRAPI_URL`, `STRAPI_API_TOKEN`
|
|
46
|
+
Optional: `GROQ_MODEL` (default: llama-3.3-70b-versatile), `NEWSAPI_QUERY`, `LIFEINSUR_NEWSAPI_QUERY`
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
- **Image extraction skipped**: The Python pipeline extracts embedded EMF/WMF images from Excel. This is deferred in the TypeScript port — use the Python pipeline for image-heavy newsletters.
|
|
50
|
+
- **Slug uniqueness**: Slugs include a timestamp (YYYYMMDDTHHMMSS) to avoid conflicts on same-day reruns.
|
|
51
|
+
- **Excel column order matters**: "contact info" matcher runs before "name" to avoid disambiguation issues.
|
|
52
|
+
- **Strapi rate limits**: API token does not rate-limit, but admin login does (5 attempts then 429 for ~2min).
|
|
53
|
+
- **ExcelJS dependency**: Only loaded dynamically when `--excel` is provided, so the dep is optional for non-CRE newsletters.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generate-newsletter-insurance
|
|
3
|
+
description: Generate an insurance-specific newsletter for Anchor Point Insurance Co. (brand=LIFEINSUR)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Generates a branded newsletter for Anchor Point Insurance Co. (brand=LIFEINSUR). This is a specialization of the generate-newsletter skill, pre-configured for the insurance vertical — it fetches life insurance and financial planning news, uses the Anchor Point brand palette (warm charcoal #44403E, terracotta #AD7C59, warm beige #FCF9F6), and omits property listings (which are CRE-11TRUST only).
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **date** (optional): Edition date as YYYY-MM-DD. Default: today.
|
|
11
|
+
- **dry-run** (optional): Generate content but do NOT push to Strapi. Useful for previewing.
|
|
12
|
+
- **news-query** (optional): Override the default NewsAPI query. Default: `LIFEINSUR_NEWSAPI_QUERY` env var or `"life insurance financial planning south florida"`.
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
1. Call `lib/newsletter/generate-insurance.ts::generateInsuranceNewsletter(options?)` to orchestrate
|
|
16
|
+
2. **Load LIFEINSUR brand config** — palette, sender email (from `brand-config` in Strapi or hardcoded defaults), display name "Anchor Point Insurance Co."
|
|
17
|
+
3. **Fetch news** — `fetchNews(query)` hits NewsAPI for 5 latest articles matching insurance/financial planning topics
|
|
18
|
+
4. **Generate AI content** — `generateAiContent(null, news)` sends news to Groq (no properties for LIFEINSUR). Returns market overview and news summaries.
|
|
19
|
+
5. **Build HTML** — `buildHtml()` assembles responsive email-safe HTML with Anchor Point brand colors and insurance-specific sections
|
|
20
|
+
6. **Build Strapi payload** — `buildStrapiPayload()` with brand=`LIFEINSUR`, slug includes timestamp for uniqueness
|
|
21
|
+
7. **Push to Strapi** — `strapiPost('/api/newsletters', data)` creates a draft newsletter (skipped in dry-run mode)
|
|
22
|
+
8. Log execution via `lib/kanban.ts::logSkillExecution()`
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
```
|
|
26
|
+
Brand: LIFEINSUR (Anchor Point Insurance Co.)
|
|
27
|
+
News articles fetched: 5
|
|
28
|
+
AI content generated: market_overview (342 words), 5 news summaries
|
|
29
|
+
HTML length: 12,847 chars
|
|
30
|
+
Strapi draft created: documentId=abc123-def456
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## CLI Usage
|
|
34
|
+
```bash
|
|
35
|
+
# Generate insurance newsletter for today
|
|
36
|
+
optimal generate-newsletter-insurance
|
|
37
|
+
|
|
38
|
+
# Specific date
|
|
39
|
+
optimal generate-newsletter-insurance --date 2026-03-01
|
|
40
|
+
|
|
41
|
+
# Preview without pushing to Strapi
|
|
42
|
+
optimal generate-newsletter-insurance --dry-run
|
|
43
|
+
|
|
44
|
+
# Custom news query
|
|
45
|
+
optimal generate-newsletter-insurance --news-query "florida insurance market rates"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Environment
|
|
49
|
+
Requires: `GROQ_API_KEY`, `NEWSAPI_KEY`, `STRAPI_URL`, `STRAPI_API_TOKEN`
|
|
50
|
+
Optional: `GROQ_MODEL` (default: llama-3.3-70b-versatile), `LIFEINSUR_NEWSAPI_QUERY`
|
|
51
|
+
|
|
52
|
+
## Gotchas
|
|
53
|
+
- **No properties**: Unlike CRE-11TRUST, LIFEINSUR newsletters do not include property listings. The `--excel` parameter is not available.
|
|
54
|
+
- **Slug uniqueness**: Slugs include a timestamp (YYYYMMDDTHHMMSS) to avoid conflicts on same-day reruns.
|
|
55
|
+
- **Brand palette**: Primary #44403E (warm charcoal), Accent #AD7C59 (terracotta), BG #FCF9F6 (warm beige).
|
|
56
|
+
- **Preview site**: Published newsletters render at https://newsletter.op-hub.com/lifeinsur
|
|
57
|
+
|
|
58
|
+
## Status
|
|
59
|
+
Implementation status: Not yet implemented. Spec only. Lib function `lib/newsletter/generate-insurance.ts` to be ported from `generate-newsletter-lifeinsur.py` in the newsletter-automation repo.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generate-social-posts
|
|
3
|
+
description: Analyze competitor ads, generate 9 branded social posts with AI copy and Unsplash photos
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
End-to-end social media post generation pipeline. Scrapes competitor ads from Meta Ad Library for pattern analysis, generates 9 themed social posts with AI-written copy, sources stock photography from Unsplash, and pushes drafts to Strapi CMS. This is a weekly workflow — typically run as "Generate 9 new social posts for [BRAND] for the week of [DATE]".
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **brand** (required): Brand key — `CRE-11TRUST` or `LIFEINSUR`
|
|
11
|
+
- **week** (required): Week start date as YYYY-MM-DD (e.g., `2026-03-03` for the week of March 3rd)
|
|
12
|
+
- **competitors** (optional): Comma-separated competitor names or path to file. Default: uses the brand's standard competitor list.
|
|
13
|
+
- **count** (optional): Number of posts to generate. Default: `9`.
|
|
14
|
+
- **dry-run** (optional): Generate content but do NOT push to Strapi.
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
1. Call `lib/social/post-generator.ts::generateSocialPosts(brand, week, options?)` to orchestrate the full pipeline
|
|
18
|
+
2. **Scrape competitor ads** — run `lib/social/scraper.ts::scrapeAds(competitors)` against the brand's competitor list (3 parallel batches of 6 companies)
|
|
19
|
+
3. **Analyze ad patterns** — extract common themes, CTAs, platforms, and copy structures from scraped data
|
|
20
|
+
4. **Generate post copy** — send analysis to Groq AI to generate 9 posts with: headline, body, cta_text, cta_url, platform targeting, overlay_style, and scheduling (spread across the week)
|
|
21
|
+
5. **Source photos** — for each post theme, search Unsplash via `unsplash.com/napi/search/photos?query=X&per_page=3` and select the best match
|
|
22
|
+
6. **Build Strapi payloads** — create `social-post` entries with all fields: brand, headline, body, cta_text, cta_url, image_url, overlay_style, template, scheduled_date, competitor_ref, platform, delivery_status=pending
|
|
23
|
+
7. **Push to Strapi** — `strapiPost('/api/social-posts', data)` for each post (skipped in dry-run mode)
|
|
24
|
+
8. Log execution via `lib/kanban.ts::logSkillExecution()`
|
|
25
|
+
|
|
26
|
+
## Output
|
|
27
|
+
```
|
|
28
|
+
Brand: LIFEINSUR (Anchor Point Insurance Co.)
|
|
29
|
+
Competitors scraped: 18 companies, 1,382 ads analyzed
|
|
30
|
+
Posts generated: 9
|
|
31
|
+
|
|
32
|
+
| # | Platform | Headline | Scheduled | Overlay |
|
|
33
|
+
|---|----------|----------|-----------|---------|
|
|
34
|
+
| 1 | instagram | "Protect What Matters Most" | Mon 3/3 | dark-bottom |
|
|
35
|
+
| 2 | facebook | "Your Family's Future..." | Mon 3/3 | brand-bottom |
|
|
36
|
+
| 3 | instagram | "Life Insurance Myths..." | Tue 3/4 | brand-full |
|
|
37
|
+
| ... | ... | ... | ... | ... |
|
|
38
|
+
|
|
39
|
+
Pushed 9 drafts to Strapi.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## CLI Usage
|
|
43
|
+
```bash
|
|
44
|
+
# Generate 9 posts for Anchor Point Insurance
|
|
45
|
+
optimal generate-social-posts --brand LIFEINSUR --week 2026-03-03
|
|
46
|
+
|
|
47
|
+
# CRE brand with custom competitors
|
|
48
|
+
optimal generate-social-posts --brand CRE-11TRUST --week 2026-03-03 --competitors "CBRE,JLL,Cushman"
|
|
49
|
+
|
|
50
|
+
# Dry run, custom count
|
|
51
|
+
optimal generate-social-posts --brand LIFEINSUR --week 2026-03-03 --count 6 --dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Environment
|
|
55
|
+
Requires: `GROQ_API_KEY`, `STRAPI_URL`, `STRAPI_API_TOKEN`, `playwright` (for ad scraping)
|
|
56
|
+
Optional: `GROQ_MODEL` (default: llama-3.3-70b-versatile)
|
|
57
|
+
|
|
58
|
+
## Gotchas
|
|
59
|
+
- **Unsplash API**: Use `unsplash.com/napi/search/photos` (public search is bot-blocked by Anubis challenge page).
|
|
60
|
+
- **Scraper batches**: Run in 3 parallel batches of 6 companies each for optimal throughput.
|
|
61
|
+
- **Overlay styles**: `dark-bottom`, `brand-bottom`, `brand-full`, `dark-full` — choose based on image content.
|
|
62
|
+
- **Platform targeting**: Posts should be spread across instagram, facebook, linkedin based on brand's platform mix.
|
|
63
|
+
- **Scheduled dates**: Spread posts across the week (e.g., 2 Mon, 2 Tue, 2 Wed, 2 Thu, 1 Fri).
|
|
64
|
+
- **Playwright browser**: Requires one-time `npx playwright install chromium`.
|
|
65
|
+
|
|
66
|
+
## Status
|
|
67
|
+
Implementation status: Not yet implemented. Spec only. Lib function `lib/social/post-generator.ts` to be built as a multi-step orchestrator calling existing scraper and CMS functions.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: health-check
|
|
3
|
+
description: Run the health check script across all Optimal services
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Run the workstation health check script to verify the status of all Optimal services, Docker containers, and Git repositories.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
None.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
1. Execute `/home/optimal/scripts/health-check.sh` via `lib/infra/deploy.ts::healthCheck()`
|
|
14
|
+
2. Script checks each service (timeout: 30 seconds total):
|
|
15
|
+
- **n8n**: Process running check (`pgrep`)
|
|
16
|
+
- **Affine**: Docker container status + HTTP check against `https://affine.op-hub.com`
|
|
17
|
+
- **Strapi CMS**: systemd user service status + HTTP health endpoint on `127.0.0.1:1337/_health`
|
|
18
|
+
- **Git Repositories**: Fetch latest, report uncommitted changes / unpushed commits / behind remote
|
|
19
|
+
- **Docker**: systemd service status + active container count
|
|
20
|
+
- **OptimalOS**: HTTP check on `localhost:3001`
|
|
21
|
+
3. Return the full formatted output
|
|
22
|
+
|
|
23
|
+
## Output
|
|
24
|
+
Formatted status report with per-service check/warn/fail indicators:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Health Check - 2026-03-01 10:00:00
|
|
28
|
+
▸ n8n -> running on port 5678
|
|
29
|
+
▸ Affine -> running at https://affine.op-hub.com
|
|
30
|
+
▸ Strapi CMS -> running at https://strapi.op-hub.com
|
|
31
|
+
▸ Git Repos -> per-repo sync status
|
|
32
|
+
▸ Docker -> N containers active
|
|
33
|
+
▸ OptimalOS -> dev server on port 3001 (optional)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
```bash
|
|
38
|
+
optimal health-check
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Environment
|
|
42
|
+
Requires: bash, curl, git, docker, systemctl. The script at `/home/optimal/scripts/health-check.sh` must exist.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ingest-transactions
|
|
3
|
+
description: Parse & deduplicate bank CSV files into the OptimalOS transactions table
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Reads a bank-exported CSV from disk, auto-detects the bank format (Chase Checking, Chase Credit, Discover, or Generic), parses rows into normalized transactions, deduplicates against existing data using SHA-256 hashes, and batch-inserts new records into the `transactions` table on the OptimalOS Supabase instance.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **file** (required): Absolute path to the CSV file on disk
|
|
11
|
+
- **user-id** (required): Supabase user UUID to own the imported transactions
|
|
12
|
+
|
|
13
|
+
## Supported Formats
|
|
14
|
+
| Format | Detection | Sign Convention |
|
|
15
|
+
|--------|-----------|-----------------|
|
|
16
|
+
| Chase Checking | `Details, Posting Date, Description, Amount, Type, Balance` | Negative = expense |
|
|
17
|
+
| Chase Credit | `Transaction Date, Post Date, Description, Category, Type, Amount` | Negative = expense |
|
|
18
|
+
| Discover | `Trans. Date, Post Date, Description, Amount, Category` | Positive = charge (flipped) |
|
|
19
|
+
| Generic CSV | Any CSV with `date`, `description`, `amount` columns | As-is |
|
|
20
|
+
|
|
21
|
+
Amex XLSX is not yet supported in the CLI (use the OptimalOS web UI).
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
1. Read the CSV file from `--file` path
|
|
25
|
+
2. Auto-detect bank format from header row
|
|
26
|
+
3. Parse rows using format-specific normalizer (handles quoted fields, date formats, amounts with $, parentheses, commas)
|
|
27
|
+
4. Generate SHA-256 dedup hash for each row: `sha256(date|amount|normalizedDescription)[0:32]`
|
|
28
|
+
5. Query `transactions.dedup_hash` to find existing duplicates
|
|
29
|
+
6. Create an `upload_batches` record for provenance tracking
|
|
30
|
+
7. Resolve category names to `categories.id` (create if missing)
|
|
31
|
+
8. Batch-insert new rows (50 per batch)
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
```
|
|
35
|
+
Format detected: chase_credit (confidence: 1.0)
|
|
36
|
+
Parsed 247 transactions
|
|
37
|
+
Inserted: 231 | Skipped (duplicates): 16 | Failed: 0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CLI Usage
|
|
41
|
+
```bash
|
|
42
|
+
tsx bin/optimal.ts ingest-transactions --file ~/Downloads/chase-statement.csv --user-id <uuid>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Environment
|
|
46
|
+
Requires: `OPTIMAL_SUPABASE_URL`, `OPTIMAL_SUPABASE_SERVICE_KEY`
|
|
47
|
+
|
|
48
|
+
## Tables Touched
|
|
49
|
+
- `transactions` — insert new rows
|
|
50
|
+
- `upload_batches` — provenance record
|
|
51
|
+
- `categories` — resolve or create category mappings
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-cms
|
|
3
|
+
description: Create, update, list, publish, and delete content in Strapi CMS across all brands
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Full content lifecycle management for Strapi v5 CMS. Handles newsletters, social posts, and blog posts across multiple brands (CRE-11TRUST, LIFEINSUR). Used by the newsletter pipeline, social post pipeline, and portfolio blog publishing.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
- **action** (required): One of `list`, `get`, `create`, `update`, `delete`, `publish`, `unpublish`, `upload`
|
|
11
|
+
- **contentType** (required): Strapi plural name — `newsletters`, `social-posts`, or `blog-posts`
|
|
12
|
+
- **brand** (optional): Brand filter — `CRE-11TRUST` or `LIFEINSUR`
|
|
13
|
+
- **documentId** (required for update/delete/publish/unpublish): Strapi v5 documentId (UUID string, NOT numeric id)
|
|
14
|
+
- **slug** (optional for get): Look up item by slug instead of documentId
|
|
15
|
+
- **data** (required for create/update): Field values as key-value pairs
|
|
16
|
+
- **status** (optional for list): `draft` or `published` — filters by Strapi draftAndPublish status
|
|
17
|
+
- **filePath** (required for upload): Absolute path to file for upload
|
|
18
|
+
- **refData** (optional for upload): `{ ref, refId, field }` to link upload to an entry
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
1. Determine action and validate required inputs
|
|
22
|
+
2. Call the appropriate function from `lib/cms/strapi-client.ts`:
|
|
23
|
+
- **list**: `listByBrand(contentType, brand, status?)` or `strapiGet('/api/{contentType}', params)`
|
|
24
|
+
- **get**: `findBySlug(contentType, slug)` or `strapiGet('/api/{contentType}/{documentId}')`
|
|
25
|
+
- **create**: `strapiPost('/api/{contentType}', data)` — include `brand` in data
|
|
26
|
+
- **update**: `strapiPut('/api/{contentType}', documentId, data)` — uses documentId, NOT numeric id
|
|
27
|
+
- **delete**: `strapiDelete('/api/{contentType}', documentId)`
|
|
28
|
+
- **publish**: `publish(contentType, documentId)` — sets publishedAt
|
|
29
|
+
- **unpublish**: `unpublish(contentType, documentId)` — clears publishedAt
|
|
30
|
+
- **upload**: `strapiUploadFile(filePath, refData?)`
|
|
31
|
+
3. Return the result with documentId, title/headline, and status
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
- **list**: Count and table of items with documentId, title, brand, status, updatedAt
|
|
35
|
+
- **get**: Full item fields
|
|
36
|
+
- **create**: `Created {contentType}: {documentId} — "{title}"`
|
|
37
|
+
- **update**: `Updated {contentType}: {documentId}`
|
|
38
|
+
- **delete**: `Deleted {contentType}: {documentId}`
|
|
39
|
+
- **publish/unpublish**: `Published/Unpublished {contentType}: {documentId}`
|
|
40
|
+
- **upload**: Uploaded file URL(s)
|
|
41
|
+
|
|
42
|
+
## Environment
|
|
43
|
+
Requires: `STRAPI_URL`, `STRAPI_API_TOKEN`
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
- **documentId, not id**: Strapi v5 PUT/DELETE use documentId (UUID string). The numeric `id` field exists but should not be used for mutations.
|
|
47
|
+
- **Reserved fields**: Never use `status`, `published_at`, `locale`, or `meta` as custom field names — Strapi v5 reserves them.
|
|
48
|
+
- **Slug uniqueness**: Include a timestamp in slugs for same-day reruns to avoid conflicts.
|
|
49
|
+
- **draftAndPublish**: Drafts are created by default. Explicitly call `publish` to make content live.
|
|
50
|
+
- **Rate limits**: Strapi admin login rate-limits aggressively (5 attempts then 429). The API token does not have this issue.
|