optimal-cli 1.0.1 → 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.
Files changed (185) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/.env.example +17 -0
  4. package/CLAUDE.md +67 -0
  5. package/COMMANDS.md +264 -0
  6. package/PUBLISH.md +70 -0
  7. package/agents/content-ops.md +2 -2
  8. package/agents/financial-ops.md +2 -2
  9. package/agents/infra-ops.md +2 -2
  10. package/apps/.gitkeep +0 -0
  11. package/bin/optimal.ts +1418 -0
  12. package/docs/MIGRATION_NEEDED.md +37 -0
  13. package/docs/plans/.gitkeep +0 -0
  14. package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
  15. package/hooks/.gitkeep +0 -0
  16. package/lib/budget/projections.ts +561 -0
  17. package/lib/budget/scenarios.ts +312 -0
  18. package/lib/cms/publish-blog.ts +129 -0
  19. package/lib/cms/strapi-client.ts +302 -0
  20. package/lib/config/registry.ts +229 -0
  21. package/lib/config/schema.ts +58 -0
  22. package/lib/config.ts +247 -0
  23. package/lib/infra/.gitkeep +0 -0
  24. package/lib/infra/deploy.ts +70 -0
  25. package/lib/infra/migrate.ts +141 -0
  26. package/lib/kanban-obsidian.ts +232 -0
  27. package/lib/kanban-sync.ts +258 -0
  28. package/lib/kanban.ts +239 -0
  29. package/lib/newsletter/.gitkeep +0 -0
  30. package/lib/newsletter/distribute.ts +256 -0
  31. package/{dist/lib/newsletter/generate-insurance.d.ts → lib/newsletter/generate-insurance.ts} +24 -7
  32. package/lib/newsletter/generate.ts +735 -0
  33. package/lib/obsidian-tasks.ts +231 -0
  34. package/lib/returnpro/.gitkeep +0 -0
  35. package/lib/returnpro/anomalies.ts +258 -0
  36. package/lib/returnpro/audit.ts +194 -0
  37. package/lib/returnpro/diagnose.ts +400 -0
  38. package/lib/returnpro/kpis.ts +255 -0
  39. package/lib/returnpro/templates.ts +323 -0
  40. package/lib/returnpro/upload-income.ts +311 -0
  41. package/lib/returnpro/upload-netsuite.ts +696 -0
  42. package/lib/returnpro/upload-r1.ts +563 -0
  43. package/lib/social/post-generator.ts +468 -0
  44. package/lib/social/publish.ts +301 -0
  45. package/lib/social/scraper.ts +503 -0
  46. package/lib/supabase.ts +25 -0
  47. package/lib/transactions/delete-batch.ts +258 -0
  48. package/lib/transactions/ingest.ts +659 -0
  49. package/lib/transactions/stamp.ts +654 -0
  50. package/package.json +5 -18
  51. package/pnpm-workspace.yaml +3 -0
  52. package/scripts/check-table.ts +24 -0
  53. package/scripts/create-tables.ts +94 -0
  54. package/scripts/migrate-kanban.sh +28 -0
  55. package/scripts/migrate-v2.ts +78 -0
  56. package/scripts/migrate.ts +79 -0
  57. package/scripts/run-migration.ts +59 -0
  58. package/scripts/seed-board.ts +203 -0
  59. package/scripts/test-kanban.ts +21 -0
  60. package/skills/audit-financials/SKILL.md +33 -0
  61. package/skills/board-create/SKILL.md +28 -0
  62. package/skills/board-update/SKILL.md +27 -0
  63. package/skills/board-view/SKILL.md +27 -0
  64. package/skills/delete-batch/SKILL.md +77 -0
  65. package/skills/deploy/SKILL.md +40 -0
  66. package/skills/diagnose-months/SKILL.md +68 -0
  67. package/skills/distribute-newsletter/SKILL.md +58 -0
  68. package/skills/export-budget/SKILL.md +44 -0
  69. package/skills/export-kpis/SKILL.md +52 -0
  70. package/skills/generate-netsuite-template/SKILL.md +51 -0
  71. package/skills/generate-newsletter/SKILL.md +53 -0
  72. package/skills/generate-newsletter-insurance/SKILL.md +59 -0
  73. package/skills/generate-social-posts/SKILL.md +67 -0
  74. package/skills/health-check/SKILL.md +42 -0
  75. package/skills/ingest-transactions/SKILL.md +51 -0
  76. package/skills/manage-cms/SKILL.md +50 -0
  77. package/skills/manage-scenarios/SKILL.md +83 -0
  78. package/skills/migrate-db/SKILL.md +79 -0
  79. package/skills/preview-newsletter/SKILL.md +50 -0
  80. package/skills/project-budget/SKILL.md +60 -0
  81. package/skills/publish-blog/SKILL.md +70 -0
  82. package/skills/publish-social-posts/SKILL.md +70 -0
  83. package/skills/rate-anomalies/SKILL.md +62 -0
  84. package/skills/scrape-ads/SKILL.md +49 -0
  85. package/skills/stamp-transactions/SKILL.md +62 -0
  86. package/skills/upload-income-statements/SKILL.md +54 -0
  87. package/skills/upload-netsuite/SKILL.md +56 -0
  88. package/skills/upload-r1/SKILL.md +45 -0
  89. package/supabase/.temp/cli-latest +1 -0
  90. package/supabase/migrations/.gitkeep +0 -0
  91. package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
  92. package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
  93. package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
  94. package/tests/config-command-smoke.test.ts +395 -0
  95. package/tests/config-registry.test.ts +173 -0
  96. package/tsconfig.json +19 -0
  97. package/agents/profiles.json +0 -5
  98. package/dist/bin/optimal.d.ts +0 -2
  99. package/dist/bin/optimal.js +0 -1590
  100. package/dist/lib/assets/index.d.ts +0 -79
  101. package/dist/lib/assets/index.js +0 -153
  102. package/dist/lib/assets.d.ts +0 -20
  103. package/dist/lib/assets.js +0 -112
  104. package/dist/lib/auth/index.d.ts +0 -83
  105. package/dist/lib/auth/index.js +0 -146
  106. package/dist/lib/board/index.d.ts +0 -39
  107. package/dist/lib/board/index.js +0 -285
  108. package/dist/lib/board/types.d.ts +0 -111
  109. package/dist/lib/board/types.js +0 -1
  110. package/dist/lib/bot/claim.d.ts +0 -3
  111. package/dist/lib/bot/claim.js +0 -20
  112. package/dist/lib/bot/coordinator.d.ts +0 -27
  113. package/dist/lib/bot/coordinator.js +0 -178
  114. package/dist/lib/bot/heartbeat.d.ts +0 -6
  115. package/dist/lib/bot/heartbeat.js +0 -30
  116. package/dist/lib/bot/index.d.ts +0 -9
  117. package/dist/lib/bot/index.js +0 -6
  118. package/dist/lib/bot/protocol.d.ts +0 -12
  119. package/dist/lib/bot/protocol.js +0 -74
  120. package/dist/lib/bot/reporter.d.ts +0 -3
  121. package/dist/lib/bot/reporter.js +0 -27
  122. package/dist/lib/bot/skills.d.ts +0 -26
  123. package/dist/lib/bot/skills.js +0 -69
  124. package/dist/lib/budget/projections.d.ts +0 -115
  125. package/dist/lib/budget/projections.js +0 -384
  126. package/dist/lib/budget/scenarios.d.ts +0 -93
  127. package/dist/lib/budget/scenarios.js +0 -214
  128. package/dist/lib/cms/publish-blog.d.ts +0 -62
  129. package/dist/lib/cms/publish-blog.js +0 -74
  130. package/dist/lib/cms/strapi-client.d.ts +0 -123
  131. package/dist/lib/cms/strapi-client.js +0 -213
  132. package/dist/lib/config/registry.d.ts +0 -17
  133. package/dist/lib/config/registry.js +0 -182
  134. package/dist/lib/config/schema.d.ts +0 -31
  135. package/dist/lib/config/schema.js +0 -25
  136. package/dist/lib/config.d.ts +0 -55
  137. package/dist/lib/config.js +0 -206
  138. package/dist/lib/errors.d.ts +0 -25
  139. package/dist/lib/errors.js +0 -91
  140. package/dist/lib/format.d.ts +0 -28
  141. package/dist/lib/format.js +0 -98
  142. package/dist/lib/infra/deploy.d.ts +0 -29
  143. package/dist/lib/infra/deploy.js +0 -58
  144. package/dist/lib/infra/migrate.d.ts +0 -34
  145. package/dist/lib/infra/migrate.js +0 -103
  146. package/dist/lib/newsletter/distribute.d.ts +0 -52
  147. package/dist/lib/newsletter/distribute.js +0 -193
  148. package/dist/lib/newsletter/generate-insurance.js +0 -36
  149. package/dist/lib/newsletter/generate.d.ts +0 -104
  150. package/dist/lib/newsletter/generate.js +0 -571
  151. package/dist/lib/returnpro/anomalies.d.ts +0 -64
  152. package/dist/lib/returnpro/anomalies.js +0 -166
  153. package/dist/lib/returnpro/audit.d.ts +0 -32
  154. package/dist/lib/returnpro/audit.js +0 -147
  155. package/dist/lib/returnpro/diagnose.d.ts +0 -52
  156. package/dist/lib/returnpro/diagnose.js +0 -281
  157. package/dist/lib/returnpro/kpis.d.ts +0 -32
  158. package/dist/lib/returnpro/kpis.js +0 -192
  159. package/dist/lib/returnpro/templates.d.ts +0 -48
  160. package/dist/lib/returnpro/templates.js +0 -229
  161. package/dist/lib/returnpro/upload-income.d.ts +0 -25
  162. package/dist/lib/returnpro/upload-income.js +0 -235
  163. package/dist/lib/returnpro/upload-netsuite.d.ts +0 -37
  164. package/dist/lib/returnpro/upload-netsuite.js +0 -566
  165. package/dist/lib/returnpro/upload-r1.d.ts +0 -48
  166. package/dist/lib/returnpro/upload-r1.js +0 -398
  167. package/dist/lib/returnpro/validate.d.ts +0 -37
  168. package/dist/lib/returnpro/validate.js +0 -124
  169. package/dist/lib/social/meta.d.ts +0 -90
  170. package/dist/lib/social/meta.js +0 -160
  171. package/dist/lib/social/post-generator.d.ts +0 -83
  172. package/dist/lib/social/post-generator.js +0 -333
  173. package/dist/lib/social/publish.d.ts +0 -66
  174. package/dist/lib/social/publish.js +0 -226
  175. package/dist/lib/social/scraper.d.ts +0 -67
  176. package/dist/lib/social/scraper.js +0 -361
  177. package/dist/lib/supabase.d.ts +0 -4
  178. package/dist/lib/supabase.js +0 -20
  179. package/dist/lib/transactions/delete-batch.d.ts +0 -60
  180. package/dist/lib/transactions/delete-batch.js +0 -203
  181. package/dist/lib/transactions/ingest.d.ts +0 -43
  182. package/dist/lib/transactions/ingest.js +0 -555
  183. package/dist/lib/transactions/stamp.d.ts +0 -51
  184. package/dist/lib/transactions/stamp.js +0 -524
  185. package/docs/CLI-REFERENCE.md +0 -361
@@ -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.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: manage-scenarios
3
+ description: Save, load, compare, and delete named budget scenarios for Wes projections
4
+ ---
5
+
6
+ ## Purpose
7
+ Manages named budget scenarios for the Wes dashboard budget projection system. Scenarios are snapshots of `fpa_yield_assumptions` data (unit forecasts, WIP units, yield rates) that can be saved, loaded, compared side-by-side, and shared across users. This enables what-if analysis — e.g., "optimistic Q2" vs "conservative Q2" scenarios.
8
+
9
+ ## Inputs
10
+ - **action** (required): One of `save`, `load`, `list`, `compare`, `delete`.
11
+ - **name** (required for save/load/delete): Scenario name (e.g., `"optimistic-q2-2026"`, `"baseline-fy25"`).
12
+ - **fiscal-year** (optional): Fiscal year filter. Default: current fiscal year.
13
+ - **user-id** (optional): User UUID for scenario ownership. Default: Carlos's user ID.
14
+ - **compare-with** (required for compare): Second scenario name to compare against.
15
+ - **format** (optional): Output format for compare/list — `table` (default) or `csv`.
16
+
17
+ ## Steps
18
+ 1. Call `lib/budget/scenarios.ts::manageScenarios(action, options)` to orchestrate
19
+ 2. **save**: Snapshot current `fpa_yield_assumptions` rows (filtered by user + fiscal year) into a named scenario record, stored as JSON blob with metadata (name, created_at, user_id, row_count)
20
+ 3. **load**: Restore a saved scenario by overwriting `fpa_yield_assumptions` rows for the target user + fiscal year with the snapshot data. Creates a backup of current data first.
21
+ 4. **list**: Show all saved scenarios with name, created_at, fiscal_year, row_count, user
22
+ 5. **compare**: Load two scenarios side-by-side and compute deltas — total units, revenue projection, per-program differences
23
+ 6. **delete**: Remove a named scenario (with confirmation)
24
+ 7. Log execution via `lib/kanban.ts::logSkillExecution()`
25
+
26
+ ## Output
27
+ **list**:
28
+ ```
29
+ | Name | FY | Created | Rows | User |
30
+ |------|----|---------|------|------|
31
+ | baseline-fy25 | FY25 | 2026-01-15 | 1,264 | Carlos |
32
+ | optimistic-q2 | FY26 | 2026-02-20 | 1,310 | Carlos |
33
+ ```
34
+
35
+ **compare**:
36
+ ```
37
+ Comparing: baseline-fy25 vs optimistic-q2
38
+
39
+ | Program | Baseline Units | Optimistic Units | Delta | Delta % |
40
+ |---------|---------------|-----------------|-------|---------|
41
+ | BRTON-WM | 42,000 | 48,500 | +6,500 | ↑15.5% |
42
+ | FORTX-POOL | 18,200 | 16,800 | -1,400 | ↓-7.7% |
43
+ | Total | 7,352,022 | 7,891,450 | +539,428 | ↑7.3% |
44
+ ```
45
+
46
+ **save**: `Saved scenario "optimistic-q2" (1,310 rows, FY26)`
47
+
48
+ **load**: `Loaded scenario "baseline-fy25" → overwrote 1,264 rows (backup: auto-backup-20260301T100000)`
49
+
50
+ ## CLI Usage
51
+ ```bash
52
+ # Save current assumptions as a named scenario
53
+ optimal manage-scenarios save --name optimistic-q2 --fiscal-year FY26
54
+
55
+ # Load a saved scenario
56
+ optimal manage-scenarios load --name baseline-fy25
57
+
58
+ # List all scenarios
59
+ optimal manage-scenarios list
60
+
61
+ # Compare two scenarios
62
+ optimal manage-scenarios compare --name baseline-fy25 --compare-with optimistic-q2
63
+
64
+ # Delete a scenario
65
+ optimal manage-scenarios delete --name old-test-scenario
66
+ ```
67
+
68
+ ## Environment
69
+ Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
70
+
71
+ ## Tables Touched
72
+ - `fpa_yield_assumptions` — read/write unit forecasts and yield data. Unique key: `(user_id, fiscal_year, month, master_program_id)`
73
+ - `wes_imports` — baseline reference for sync validation
74
+ - Scenario storage: TBD — either a new `budget_scenarios` table or JSON blobs in an existing metadata table
75
+
76
+ ## Gotchas
77
+ - **fpa_yield_assumptions refactor**: Table was refactored Feb 2026 — dropped 3 WIP% columns, added `wip_units INTEGER`. Unique key is `(user_id, fiscal_year, month, master_program_id)`.
78
+ - **Wes sync pattern**: When baselines diverge, copy from Carlos to all other users. FY25 baseline: 7,352,022 units, 1,264 rows, 97 masters.
79
+ - **Load creates backup**: Loading a scenario automatically saves the current state as `auto-backup-{timestamp}` before overwriting.
80
+ - **User isolation**: Scenarios are per-user by default. Cross-user scenario sharing requires explicit user-id parameter.
81
+
82
+ ## Status
83
+ Implementation status: Not yet implemented. Spec only. Lib function `lib/budget/scenarios.ts` to be built on top of existing `lib/budget/projections.ts` in wes-dashboard.
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: migrate-db
3
+ description: Run Supabase database migrations across ReturnPro or OptimalOS instances
4
+ ---
5
+
6
+ ## Purpose
7
+ Executes pending database migrations against the linked Supabase instance using `supabase db push --linked`. This is the only sanctioned way to modify database schemas — Carlos never runs SQL manually. Supports both the ReturnPro instance (financial data) and the OptimalOS instance (kanban, transactions).
8
+
9
+ ## Inputs
10
+ - **instance** (required): Target Supabase instance — `returnpro` or `optimalos`.
11
+ - **migration** (optional): Specific migration file name to create before pushing (e.g., `add-budget-scenarios-table`). If provided, creates the file in `supabase/migrations/` first.
12
+ - **sql** (optional): SQL content for a new migration file. Required if `--migration` is specified.
13
+ - **dry-run** (optional): Show pending migrations without applying them.
14
+
15
+ ## Steps
16
+ 1. Call `lib/infra/migrate.ts::migrateDb(instance, options?)` to orchestrate
17
+ 2. **Resolve instance** — map `returnpro` to `/home/optimal/dashboard-returnpro` supabase config, `optimalos` to `/home/optimal/optimalos` supabase config (or the consolidated `/home/optimal/optimal-cli/supabase` config)
18
+ 3. **Create migration file** (if `--migration` + `--sql` provided):
19
+ - Generate timestamped filename: `{YYYYMMDDHHmmss}_{migration-name}.sql`
20
+ - Write SQL content to `supabase/migrations/{filename}`
21
+ 4. **List pending** — show which migrations haven't been applied yet
22
+ 5. **Push** — run `supabase db push --linked` from the appropriate project directory
23
+ 6. **Verify** — confirm migration was applied successfully
24
+ 7. Log execution via `lib/kanban.ts::logSkillExecution()`
25
+
26
+ ## Output
27
+ ```
28
+ Instance: returnpro
29
+ Project dir: /home/optimal/dashboard-returnpro
30
+ Pending migrations: 1
31
+
32
+ Applying: 20260301100000_add-budget-scenarios-table.sql
33
+ Migration applied successfully.
34
+
35
+ Current schema version: 20260301100000
36
+ ```
37
+
38
+ Dry-run:
39
+ ```
40
+ [DRY RUN] Instance: returnpro
41
+ Pending migrations:
42
+ 1. 20260301100000_add-budget-scenarios-table.sql (new)
43
+ Would run: supabase db push --linked
44
+ ```
45
+
46
+ ## CLI Usage
47
+ ```bash
48
+ # Push pending migrations to ReturnPro
49
+ optimal migrate-db --instance returnpro
50
+
51
+ # Push to OptimalOS
52
+ optimal migrate-db --instance optimalos
53
+
54
+ # Create a new migration and push
55
+ optimal migrate-db --instance returnpro --migration add-budget-scenarios --sql "CREATE TABLE budget_scenarios (...);"
56
+
57
+ # Dry run
58
+ optimal migrate-db --instance returnpro --dry-run
59
+ ```
60
+
61
+ ## Environment
62
+ Requires: `supabase` CLI installed (Homebrew, v2.72.7+). Authentication is handled internally by `supabase db push --linked`.
63
+
64
+ ## Supabase Instances
65
+
66
+ | Instance | Supabase URL | Project Directory |
67
+ |----------|-------------|-------------------|
68
+ | returnpro | vvutttwunexshxkmygik.supabase.co | /home/optimal/dashboard-returnpro |
69
+ | optimalos | hbfalrpswysryltysonm.supabase.co | /home/optimal/optimalos |
70
+
71
+ ## Gotchas
72
+ - **Never run SQL manually**: Always use migration files + `supabase db push --linked`. This is a hard rule.
73
+ - **No `supabase db execute`**: The `db execute` subcommand does not exist in Supabase CLI v2.72.7. Use migration files instead.
74
+ - **No psql directly**: The pooler has permission issues and direct IPv6 is unreachable. `db push --linked` handles auth internally.
75
+ - **stg_financials_raw.amount is TEXT**: If a migration touches this column, remember it stores amounts as TEXT strings, not NUMERIC.
76
+ - **Reserved fields in Strapi**: If migrating Strapi's underlying Postgres, avoid `status`, `published_at`, `locale`, `meta` as column names.
77
+
78
+ ## Status
79
+ Implementation status: Not yet implemented. Spec only. Lib function `lib/infra/migrate.ts` to be built as a cross-repo migration runner.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: preview-newsletter
3
+ description: Deploy the newsletter preview site to Vercel for client review
4
+ ---
5
+
6
+ ## Purpose
7
+ Deploys the newsletter-preview Next.js site to Vercel so clients and stakeholders can review newsletters and social posts in a branded web interface. This is a thin wrapper around the `deploy` skill, pre-configured for the `newsletter-preview` app. The preview site renders published Strapi content at `https://newsletter.op-hub.com`.
8
+
9
+ ## Inputs
10
+ - **prod** (optional): Deploy to production. Default: preview deployment.
11
+
12
+ ## Steps
13
+ 1. Call `lib/infra/deploy.ts::deploy('newsletter-preview', options?)` to deploy
14
+ 2. Resolves to path `/home/optimal/projects/newsletter-preview`
15
+ 3. Runs `vercel --cwd /home/optimal/projects/newsletter-preview` (or `vercel --prod` for production)
16
+ 4. Waits for deployment to complete (up to 2 minutes)
17
+ 5. Returns the deployment URL
18
+ 6. Log execution via `lib/kanban.ts::logSkillExecution()`
19
+
20
+ ## Output
21
+ ```
22
+ Deploying newsletter-preview...
23
+ Preview URL: https://newsletter-preview-abc123.vercel.app
24
+ ```
25
+
26
+ Or for production:
27
+ ```
28
+ Deploying newsletter-preview to production...
29
+ Production URL: https://newsletter.op-hub.com
30
+ ```
31
+
32
+ ## CLI Usage
33
+ ```bash
34
+ # Preview deployment
35
+ optimal preview-newsletter
36
+
37
+ # Production deployment
38
+ optimal preview-newsletter --prod
39
+ ```
40
+
41
+ ## Environment
42
+ Requires: `vercel` CLI installed globally and authenticated.
43
+
44
+ ## Gotchas
45
+ - **This is just a deploy**: No content generation happens here. Use `generate-newsletter` or `generate-newsletter-insurance` first to create content, then deploy the preview.
46
+ - **Strapi must be reachable**: The preview site fetches content from Strapi at build time and runtime. Strapi must be running at `https://strapi.op-hub.com`.
47
+ - **Brand routes**: CRE-11TRUST content at `/cre-11trust`, LIFEINSUR content at `/lifeinsur`.
48
+
49
+ ## Status
50
+ Implementation status: Not yet implemented. Spec only. Uses existing `lib/infra/deploy.ts` with `newsletter-preview` app name.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: project-budget
3
+ description: Run FY26 budget projections with percentage or flat adjustments on FY25 checked-in units
4
+ ---
5
+
6
+ ## Purpose
7
+ Generates FY26 budget projections by applying uniform adjustments (percentage or flat) to FY25 actual checked-in units. Supports both unit volume and average retail price projections, with revenue calculations (units x retail).
8
+
9
+ Data comes from either:
10
+ 1. ReturnPro Supabase `fpa_wes_imports` table (default)
11
+ 2. A JSON file of `CheckedInUnitsSummary[]` (via `--file` flag or stdin)
12
+
13
+ ## Inputs
14
+ - **adjustment-type** (optional): `percent` or `flat`. Default: `percent`.
15
+ - **adjustment-value** (optional): Numeric value for the adjustment. Default: `0` (no change).
16
+ - **format** (optional): Output format — `table` (Bloomberg-dense markdown) or `csv`. Default: `table`.
17
+ - **fiscal-year** (optional): Base fiscal year for actuals. Default: `2025`.
18
+ - **user-id** (optional): Supabase user UUID to filter imports by.
19
+ - **file** (optional): Path to a JSON file containing `CheckedInUnitsSummary[]` array. If provided, skips Supabase fetch.
20
+
21
+ ## Steps
22
+ 1. Load FY25 data: fetch from `fpa_wes_imports` or parse from JSON file
23
+ 2. Call `initializeProjections(summary)` to create baseline entries
24
+ 3. Call `applyUniformAdjustment(projections, type, value)` if adjustment specified
25
+ 4. Format output as table or CSV via `formatProjectionTable()` or `exportToCSV()`
26
+
27
+ ## Output
28
+ Table format (default) — Bloomberg-dense:
29
+
30
+ ```
31
+ FY25 Actual: 7.35M units | FY26 Projected: 7.65M units | +4.0%
32
+ Revenue: $180.2M -> $187.4M | +4.0%
33
+
34
+ | Client | Program | FY25 Units | FY26 Units | Delta | Avg Retail | Proj Retail | Rev Delta |
35
+ |--------|---------|------------|------------|-------|------------|-------------|-----------|
36
+ | Walmart | BRTON-WM-LIQ | 120.5K | 125.3K | +4.8K (4.0%) | $45.20 | $45.20 | +$217.0K |
37
+ ```
38
+
39
+ CSV format: full-precision export with unit, retail, and inventory value columns.
40
+
41
+ ## CLI Usage
42
+ ```bash
43
+ # Default: no adjustment, table format, FY25 from Supabase
44
+ npx tsx bin/optimal.ts project-budget
45
+
46
+ # 4% growth projection
47
+ npx tsx bin/optimal.ts project-budget --adjustment-type percent --adjustment-value 4
48
+
49
+ # Flat +500 units per program
50
+ npx tsx bin/optimal.ts project-budget --adjustment-type flat --adjustment-value 500
51
+
52
+ # CSV output
53
+ npx tsx bin/optimal.ts project-budget --adjustment-type percent --adjustment-value 4 --format csv
54
+
55
+ # From JSON file instead of Supabase
56
+ npx tsx bin/optimal.ts project-budget --file ./fy25-actuals.json --adjustment-type percent --adjustment-value 10
57
+ ```
58
+
59
+ ## Environment
60
+ Requires (when using Supabase): `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: publish-blog
3
+ description: Publish a blog post to Strapi CMS and deploy the portfolio site to Vercel
4
+ ---
5
+
6
+ ## Purpose
7
+ End-to-end blog publishing workflow: creates or updates a blog post in Strapi CMS, publishes it, and deploys the portfolio-2026 site to Vercel so the post goes live at carloslenis.com. Supports both manual blog posts and automated research reports (AI-generated content with specific formatting rules).
8
+
9
+ ## Inputs
10
+ - **title** (required): Blog post title.
11
+ - **slug** (optional): URL slug. Default: auto-generated from title + year (e.g., `copper-investment-thesis-ai-data-centers-2026`).
12
+ - **content** (required): Markdown content for the blog post body.
13
+ - **site** (required): Site filter — `portfolio` (carloslenis.com) or `insurance` (future).
14
+ - **tags** (optional): Comma-separated tag names (e.g., `"Automated Report,AI,Finance"`).
15
+ - **documentId** (optional): If provided, updates an existing post instead of creating a new one.
16
+ - **deploy** (optional): Auto-deploy portfolio site after publishing. Default: true.
17
+ - **draft** (optional): Create as draft without publishing. Default: false (publish immediately).
18
+
19
+ ## Steps
20
+ 1. Call `lib/cms/strapi-client.ts` functions to manage the blog post lifecycle
21
+ 2. **Build payload** — construct Strapi `blog-post` data: title, slug, content, site, tags
22
+ 3. **Create or update** — if `--documentId` given, `strapiPut('/api/blog-posts', documentId, data)`; otherwise `strapiPost('/api/blog-posts', data)`
23
+ 4. **Publish** — unless `--draft` is set, call `publish('blog-posts', documentId)`
24
+ 5. **Deploy** — unless `--deploy false`, call `lib/infra/deploy.ts::deploy('portfolio', { prod: true })` to push to Vercel production
25
+ 6. **Return** — documentId, slug, and live URL
26
+ 7. Log execution via `lib/kanban.ts::logSkillExecution()`
27
+
28
+ ## Output
29
+ ```
30
+ Blog post published: "Copper Investment Thesis for AI Data Centers"
31
+ Slug: copper-investment-thesis-ai-data-centers-2026
32
+ DocumentId: abc123-def456
33
+ Site: portfolio
34
+ Tags: Automated Report, AI, Finance
35
+ Deploy: production → https://carloslenis.com
36
+ ```
37
+
38
+ ## CLI Usage
39
+ ```bash
40
+ # Publish a new blog post and deploy
41
+ optimal publish-blog --title "My Post" --content ./post.md --site portfolio --tags "Finance"
42
+
43
+ # Create as draft (no deploy)
44
+ optimal publish-blog --title "Draft Post" --content ./draft.md --site portfolio --draft
45
+
46
+ # Update existing post
47
+ optimal publish-blog --documentId abc123 --content ./updated.md --deploy
48
+ ```
49
+
50
+ ## Automated Report Format
51
+ When publishing AI-generated research reports, the content MUST follow these rules:
52
+ - **Tag**: `"Automated Report"` (never pretend Carlos wrote it)
53
+ - **Structure**: Disclosure blockquote at top, `---` between every section, `## ` headings for section cards
54
+ - **References**: Numbered inline refs `[[1]](#ref-1)` with `## Sources & References` section at bottom
55
+ - **Tables**: Use heavily to break up text (data tables, comparisons, scorecards)
56
+ - **Links**: Every data claim must be hyperlinked to its source
57
+ - **Slug format**: `topic-keywords-YYYY`
58
+
59
+ The portfolio site's `BlogContent` component auto-detects multi-section posts (4+ sections with `---`) and renders each `## ` section as a themed card with colored borders.
60
+
61
+ ## Environment
62
+ Requires: `STRAPI_URL`, `STRAPI_API_TOKEN`, `vercel` CLI (for deploy)
63
+
64
+ ## Gotchas
65
+ - **documentId for updates**: Strapi v5 uses documentId (UUID), not numeric id.
66
+ - **Site field**: The `site` enum in Strapi blog-post schema determines which site renders the post. Add new sites by editing the schema.
67
+ - **Deploy after publish**: By default, the portfolio site is redeployed to production after publishing. Pass `--deploy false` to skip.
68
+
69
+ ## Status
70
+ Implementation status: Not yet implemented. Spec only. Uses existing `lib/cms/strapi-client.ts` for Strapi operations and `lib/infra/deploy.ts` for Vercel deployment.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: publish-social-posts
3
+ description: Push social post drafts from Strapi to live platforms (Instagram, Facebook, LinkedIn) via distribution hub
4
+ ---
5
+
6
+ ## Purpose
7
+ Publishes social post drafts from Strapi CMS to their target platforms (Instagram, Facebook, LinkedIn, Twitter/X). Works through the distribution hub: publishes the Strapi entry (triggers webhook), which n8n picks up to post to the actual social platforms via Meta Marketing API and other platform APIs. Handles both immediate publishing and scheduled posts.
8
+
9
+ ## Inputs
10
+ - **brand** (required): Brand key — `CRE-11TRUST` or `LIFEINSUR`
11
+ - **documentIds** (optional): Comma-separated Strapi documentIds to publish. If omitted, publishes all pending drafts for the brand.
12
+ - **schedule-only** (optional): Only publish posts with a `scheduled_date` in the future (let the 15-minute cron handle delivery). Default: false.
13
+ - **test** (optional): Post to test/sandbox accounts instead of live pages.
14
+
15
+ ## Steps
16
+ 1. Call `lib/cms/strapi-client.ts::publishSocialPosts(brand, options?)` to orchestrate
17
+ 2. **Fetch pending posts** — if `--documentIds` provided, fetch those specific posts; otherwise `listByBrand('social-posts', brand, 'draft')` to get all pending drafts
18
+ 3. **Validate posts** — ensure each post has required fields: headline, body, image_url, platform, scheduled_date
19
+ 4. **Publish in Strapi** — `publish('social-posts', documentId)` for each post (sets publishedAt, triggers webhook)
20
+ 5. **n8n distribution** — Strapi publish webhook fires n8n workflow which:
21
+ - Reads `brand-config` for platform IDs (IG page ID, FB page ID, etc.)
22
+ - Posts to target platform via respective API
23
+ - Updates `delivery_status` (pending → scheduled/delivered/failed)
24
+ - Writes `platform_post_id` back to Strapi on success
25
+ 6. **Report results** — summarize published vs scheduled vs failed
26
+ 7. Log execution via `lib/kanban.ts::logSkillExecution()`
27
+
28
+ ## Output
29
+ ```
30
+ Brand: LIFEINSUR
31
+ Posts processed: 9
32
+
33
+ | # | Headline | Platform | Status | Post ID |
34
+ |---|----------|----------|--------|---------|
35
+ | 1 | "Protect What Matters" | instagram | delivered | 17899... |
36
+ | 2 | "Your Family's Future" | facebook | delivered | 61294... |
37
+ | 3 | "Life Insurance Myths" | instagram | scheduled (3/4) | — |
38
+ | ... | ... | ... | ... | ... |
39
+
40
+ Delivered: 4 | Scheduled: 5 | Failed: 0
41
+ ```
42
+
43
+ ## CLI Usage
44
+ ```bash
45
+ # Publish all pending LIFEINSUR posts
46
+ optimal publish-social-posts --brand LIFEINSUR
47
+
48
+ # Publish specific posts
49
+ optimal publish-social-posts --brand CRE-11TRUST --documentIds abc123,def456,ghi789
50
+
51
+ # Only set up scheduled posts (don't post immediately)
52
+ optimal publish-social-posts --brand LIFEINSUR --schedule-only
53
+
54
+ # Test mode
55
+ optimal publish-social-posts --brand LIFEINSUR --test
56
+ ```
57
+
58
+ ## Environment
59
+ Requires: `STRAPI_URL`, `STRAPI_API_TOKEN`
60
+ Platform credentials managed in n8n (Meta access token, Twitter keys, etc.), not in CLI.
61
+
62
+ ## Gotchas
63
+ - **n8n must be running**: Distribution depends on n8n catching the Strapi webhook.
64
+ - **brand-config required**: The `brand-config` content type in Strapi must have platform IDs (IG page ID, FB page ID) for the target brand.
65
+ - **Scheduling**: Posts with future `scheduled_date` are queued — a 15-minute n8n cron picks them up. Posts with past/blank `scheduled_date` are distributed immediately on publish.
66
+ - **delivery_status flow**: pending → scheduled (if future date) → delivered/failed. Partial state means some platforms succeeded and others failed.
67
+ - **Meta Marketing API**: Requires `ads_management` + `ads_read` permissions on the Meta developer app access token.
68
+
69
+ ## Status
70
+ Implementation status: Not yet implemented. Spec only. Uses existing `lib/cms/strapi-client.ts` for Strapi operations; distribution handled by n8n webhook.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: rate-anomalies
3
+ description: Flag outlier $/unit rates across programs and months in ReturnPro financial data
4
+ ---
5
+
6
+ ## Purpose
7
+ Detects anomalous per-unit rates ($/unit) across programs and months in ReturnPro's staged financial data. Compares each program-month's rate against its historical average and flags statistical outliers. This is a key data quality tool — rate anomalies often indicate data entry errors, misclassified programs, or upstream reporting issues.
8
+
9
+ ## Inputs
10
+ - **months** (optional): Comma-separated YYYY-MM strings to analyze. Default: last 6 months with data.
11
+ - **threshold** (optional): Z-score threshold for flagging anomalies. Default: `2.0` (2 standard deviations).
12
+ - **programs** (optional): Comma-separated program name substrings to filter. Default: all programs.
13
+ - **format** (optional): Output format — `table` (default) or `csv`.
14
+
15
+ ## Steps
16
+ 1. Call `lib/returnpro/anomalies.ts::detectRateAnomalies(options?)` to fetch and analyze data
17
+ 2. Query `stg_financials_raw` for revenue and unit rows across the target months
18
+ 3. Compute $/unit rate for each program-month combination
19
+ 4. Calculate historical mean and standard deviation per program
20
+ 5. Flag any program-month where the rate's z-score exceeds the threshold
21
+ 6. Sort flagged anomalies by severity (highest z-score first)
22
+ 7. Log execution via `lib/kanban.ts::logSkillExecution()`
23
+
24
+ ## Output
25
+ Bloomberg-dense table with inline severity indicators:
26
+
27
+ | Program | Month | Rate | Avg Rate | Z-Score | Delta |
28
+ |---------|-------|------|----------|---------|-------|
29
+ | BRTON-WM | 2026-01 | $3.42/u | $2.10/u | 3.1 | ↑62.9% |
30
+ | FORTX-POOL | 2025-12 | $0.89/u | $1.55/u | -2.4 | ↓-42.6% |
31
+
32
+ Rows with z-score > 3.0 get `bg-red-500/5` row tint indicator. All numbers are `font-mono`, right-aligned.
33
+
34
+ ## CLI Usage
35
+ ```bash
36
+ # Default: last 6 months, z-score threshold 2.0
37
+ optimal rate-anomalies
38
+
39
+ # Specific months, stricter threshold
40
+ optimal rate-anomalies --months 2026-01,2025-12 --threshold 1.5
41
+
42
+ # Filter to specific programs
43
+ optimal rate-anomalies --programs BRTON,FORTX
44
+
45
+ # CSV export
46
+ optimal rate-anomalies --format csv > anomalies.csv
47
+ ```
48
+
49
+ ## Environment
50
+ Requires: `RETURNPRO_SUPABASE_URL`, `RETURNPRO_SUPABASE_SERVICE_KEY`
51
+
52
+ ## Tables Touched
53
+ - `stg_financials_raw` — read revenue and unit data (CAST amount from TEXT)
54
+ - `dim_master_program` — resolve program names
55
+
56
+ ## Gotchas
57
+ - **amount is TEXT**: Always CAST `stg_financials_raw.amount` before numeric operations.
58
+ - **Reference implementation**: The Bloomberg-dense rate anomaly explorer UI is at `components/analysis/rate-anomaly-explorer.tsx` in dashboard-returnpro (Feb 2026 redesign).
59
+ - **Sign conventions**: Revenue is positive, expenses are negative. Unit counts are always positive.
60
+
61
+ ## Status
62
+ Implementation status: Not yet implemented. Spec only. Lib function `lib/returnpro/anomalies.ts` to be extracted from dashboard-returnpro's `/api/analytics/rate-anomalies` route.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: scrape-ads
3
+ description: Scrape Meta Ad Library for competitor ad intelligence using headless Playwright browser
4
+ ---
5
+
6
+ ## Purpose
7
+ Scrape the Facebook/Meta Ad Library to extract competitor ad data for analysis. Uses headless Chromium with anti-detection measures (disabled automation features, realistic user agent, viewport 1920x1080). Extracts ad metadata including Library ID, status, start date, ad copy, platforms, impressions, spend, and media type.
8
+
9
+ ## Inputs
10
+ - **companies** (required): Comma-separated list of company names, OR a path to a text file with one company per line
11
+ - **output** (optional): File path to save CSV results (default: stdout)
12
+ - **batch-size** (optional): Number of companies per batch (default: 6). Companies run sequentially within a batch with 4s delay between each.
13
+
14
+ ## Steps
15
+ 1. **Parse companies** — accept CSV string or read from file (one company per line)
16
+ 2. **Launch browser** — headless Chromium with anti-detection args
17
+ 3. **Batch processing** — split companies into batches of `batch-size`, each batch gets a fresh browser context
18
+ 4. **Per company**: navigate to Ad Library search URL, wait for load, scroll to load all ads (up to 15 scrolls with 2s delay each)
19
+ 5. **Extract ads** — two-stage: first try DOM querySelectorAll for divs containing exactly one `Library ID: \d+`, then fallback to splitting full page text by Library ID boundaries
20
+ 6. **Parse metadata** — regex extraction of Library ID, start date, status, page name, ad text, impressions, spend, media type, platforms
21
+ 7. **Extract landing URLs** — scan DOM for `l.facebook.com` redirect links and associate with ad IDs
22
+ 8. **Output CSV** — columns: company_searched, ad_id, page_name, ad_text, status, start_date, impressions, spend, media_type, platforms, landing_page_url, full_text_snippet
23
+
24
+ ## Output
25
+ - CSV data (to stdout or file) with one row per ad
26
+ - Console progress logs during scraping
27
+
28
+ ## CLI Usage
29
+ ```bash
30
+ # Scrape specific companies (stdout)
31
+ optimal scrape-ads --companies "State Farm,Allstate,GEICO"
32
+
33
+ # Scrape from file, save to CSV
34
+ optimal scrape-ads --companies ~/projects/meta-ad-scraper/data/companies.txt --output ./ads.csv
35
+
36
+ # Custom batch size
37
+ optimal scrape-ads --companies "Company A,Company B" --batch-size 3
38
+ ```
39
+
40
+ ## Environment
41
+ Requires: `playwright` npm package with Chromium browser installed (`npx playwright install chromium`)
42
+
43
+ ## Gotchas
44
+ - **Browser install**: Playwright requires a one-time `npx playwright install chromium` to download the browser binary. The scraper will fail if this hasn't been run.
45
+ - **Rate limiting**: Facebook may rate-limit or block automated access. The 4s delay between companies and fresh contexts per batch help mitigate this.
46
+ - **Anti-detection**: Uses `--disable-blink-features=AutomationControlled` and a realistic user agent string.
47
+ - **DOM extraction**: The primary extraction strategy looks for div elements containing exactly one Library ID with text length between 50-5000 chars, deduplicated by Library ID. Falls back to text splitting if DOM strategy finds nothing.
48
+ - **Batch strategy**: Per memory, run in 3 parallel batches of 6 companies each for optimal throughput.
49
+ - **No actual execution in CI**: This scraper hits live Facebook servers. Do not run in automated pipelines without explicit intent.