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,258 @@
1
+ /**
2
+ * Transaction & Staging Batch Deletion — Safe Preview and Execute
3
+ *
4
+ * Provides safe batch deletion of transactions (OptimalOS) and staging
5
+ * financials (ReturnPro) with preview mode defaulting to dryRun=true.
6
+ *
7
+ * Tables:
8
+ * - transactions → OptimalOS Supabase (getSupabase('optimal'))
9
+ * - stg_financials_raw → ReturnPro Supabase (getSupabase('returnpro'))
10
+ *
11
+ * Columns:
12
+ * transactions: id, user_id, date, description, amount, category, source, stamp_match_type, created_at
13
+ * stg_financials_raw: id, account_code, account_name, amount (TEXT), month (YYYY-MM), source, user_id, created_at
14
+ */
15
+
16
+ import 'dotenv/config'
17
+ import { getSupabase } from '../supabase.js'
18
+ import type { SupabaseClient } from '@supabase/supabase-js'
19
+
20
+ // =============================================================================
21
+ // TYPES
22
+ // =============================================================================
23
+
24
+ export interface DeleteBatchOptions {
25
+ table: 'transactions' | 'stg_financials_raw'
26
+ userId?: string // required for transactions
27
+ filters: {
28
+ dateFrom?: string // YYYY-MM-DD (maps to `date` on transactions, derived from `month` on staging)
29
+ dateTo?: string // YYYY-MM-DD
30
+ source?: string // e.g. 'Chase', 'Discover'
31
+ category?: string // transaction category
32
+ accountCode?: string // for stg_financials_raw
33
+ month?: string // YYYY-MM for stg_financials_raw
34
+ }
35
+ dryRun?: boolean // default true — must explicitly set false to delete
36
+ }
37
+
38
+ export interface DeleteBatchResult {
39
+ table: string
40
+ deletedCount: number
41
+ dryRun: boolean
42
+ filters: Record<string, string>
43
+ }
44
+
45
+ export interface PreviewResult {
46
+ table: string
47
+ matchCount: number
48
+ sample: Array<Record<string, unknown>>
49
+ groupedCounts: Record<string, number>
50
+ }
51
+
52
+ // =============================================================================
53
+ // INTERNAL HELPERS
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Return the correct Supabase client for the given table.
58
+ */
59
+ function getClientForTable(table: DeleteBatchOptions['table']): SupabaseClient {
60
+ return table === 'transactions'
61
+ ? getSupabase('optimal')
62
+ : getSupabase('returnpro')
63
+ }
64
+
65
+ /**
66
+ * Apply the shared set of filters to a Supabase query builder.
67
+ * Works for both SELECT and DELETE queries because both are PostgREST filters.
68
+ *
69
+ * For `stg_financials_raw`:
70
+ * - dateFrom / dateTo are ignored (use `month` instead)
71
+ * - month is applied as an eq filter on the `month` column
72
+ * - accountCode is applied as an eq filter on `account_code`
73
+ *
74
+ * For `transactions`:
75
+ * - dateFrom / dateTo are applied as gte/lte on `date`
76
+ * - source is applied as an eq filter on `source`
77
+ * - category is applied as an eq filter on `category`
78
+ * - userId is applied as an eq filter on `user_id`
79
+ */
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ function applyFilters<T extends Record<string, any>>(
82
+ query: T,
83
+ table: DeleteBatchOptions['table'],
84
+ userId: string | undefined,
85
+ filters: DeleteBatchOptions['filters'],
86
+ ): T {
87
+ let q = query
88
+
89
+ if (table === 'transactions') {
90
+ if (userId) q = (q as unknown as { eq(col: string, val: string): T }).eq('user_id', userId) as unknown as T
91
+ if (filters.dateFrom) q = (q as unknown as { gte(col: string, val: string): T }).gte('date', filters.dateFrom) as unknown as T
92
+ if (filters.dateTo) q = (q as unknown as { lte(col: string, val: string): T }).lte('date', filters.dateTo) as unknown as T
93
+ if (filters.source) q = (q as unknown as { eq(col: string, val: string): T }).eq('source', filters.source) as unknown as T
94
+ if (filters.category) q = (q as unknown as { eq(col: string, val: string): T }).eq('category', filters.category) as unknown as T
95
+ } else {
96
+ // stg_financials_raw
97
+ if (userId) q = (q as unknown as { eq(col: string, val: string): T }).eq('user_id', userId) as unknown as T
98
+ if (filters.month) q = (q as unknown as { eq(col: string, val: string): T }).eq('month', filters.month) as unknown as T
99
+ if (filters.accountCode) q = (q as unknown as { eq(col: string, val: string): T }).eq('account_code', filters.accountCode) as unknown as T
100
+ if (filters.source) q = (q as unknown as { eq(col: string, val: string): T }).eq('source', filters.source) as unknown as T
101
+ }
102
+
103
+ return q
104
+ }
105
+
106
+ /**
107
+ * Serialize active filters for the result record (human-readable).
108
+ */
109
+ function serializeFilters(
110
+ table: DeleteBatchOptions['table'],
111
+ userId: string | undefined,
112
+ filters: DeleteBatchOptions['filters'],
113
+ ): Record<string, string> {
114
+ const out: Record<string, string> = {}
115
+ if (userId) out.user_id = userId
116
+ if (table === 'transactions') {
117
+ if (filters.dateFrom) out.dateFrom = filters.dateFrom
118
+ if (filters.dateTo) out.dateTo = filters.dateTo
119
+ if (filters.source) out.source = filters.source
120
+ if (filters.category) out.category = filters.category
121
+ } else {
122
+ if (filters.month) out.month = filters.month
123
+ if (filters.accountCode) out.accountCode = filters.accountCode
124
+ if (filters.source) out.source = filters.source
125
+ }
126
+ return out
127
+ }
128
+
129
+ // =============================================================================
130
+ // PUBLIC FUNCTIONS
131
+ // =============================================================================
132
+
133
+ /**
134
+ * Preview what would be deleted without touching any data.
135
+ *
136
+ * Returns:
137
+ * - matchCount: total rows matching the filters
138
+ * - sample: first 10 matching rows
139
+ * - groupedCounts: row counts grouped by `source` (transactions) or `account_code` (staging)
140
+ */
141
+ export async function previewBatch(opts: DeleteBatchOptions): Promise<PreviewResult> {
142
+ const { table, userId, filters } = opts
143
+ const supabase = getClientForTable(table)
144
+
145
+ // --- Count matching rows ---
146
+ const countQuery = supabase
147
+ .from(table)
148
+ .select('*', { count: 'exact', head: true })
149
+
150
+ const countQueryWithFilters = applyFilters(countQuery, table, userId, filters)
151
+ const { count, error: countError } = await countQueryWithFilters
152
+
153
+ if (countError) {
154
+ throw new Error(`previewBatch count error on ${table}: ${countError.message}`)
155
+ }
156
+
157
+ const matchCount = count ?? 0
158
+
159
+ // --- Fetch sample rows (first 10) ---
160
+ const sampleQuery = supabase
161
+ .from(table)
162
+ .select('*')
163
+ .limit(10)
164
+
165
+ const sampleQueryWithFilters = applyFilters(sampleQuery, table, userId, filters)
166
+ const { data: sampleData, error: sampleError } = await sampleQueryWithFilters
167
+
168
+ if (sampleError) {
169
+ throw new Error(`previewBatch sample error on ${table}: ${sampleError.message}`)
170
+ }
171
+
172
+ const sample = (sampleData ?? []) as Array<Record<string, unknown>>
173
+
174
+ // --- Grouped counts ---
175
+ // Group by `source` for transactions, `account_code` for staging
176
+ const groupCol = table === 'transactions' ? 'source' : 'account_code'
177
+
178
+ const groupQuery = supabase
179
+ .from(table)
180
+ .select(groupCol)
181
+
182
+ const groupQueryWithFilters = applyFilters(groupQuery, table, userId, filters)
183
+ const { data: groupData, error: groupError } = await groupQueryWithFilters
184
+
185
+ if (groupError) {
186
+ throw new Error(`previewBatch group error on ${table}: ${groupError.message}`)
187
+ }
188
+
189
+ const groupedCounts: Record<string, number> = {}
190
+ for (const row of (groupData ?? []) as Array<Record<string, unknown>>) {
191
+ const key = (row[groupCol] as string | null | undefined) ?? '(unknown)'
192
+ groupedCounts[key] = (groupedCounts[key] ?? 0) + 1
193
+ }
194
+
195
+ return {
196
+ table,
197
+ matchCount,
198
+ sample,
199
+ groupedCounts,
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Delete matching rows in batch — or preview them without deleting (dryRun=true).
205
+ *
206
+ * Safety: dryRun defaults to TRUE. Caller must explicitly pass dryRun=false
207
+ * to execute an actual deletion.
208
+ *
209
+ * In dryRun mode: counts matching rows and returns deletedCount=0.
210
+ * In execute mode: issues a Supabase DELETE with the same filters and returns
211
+ * the number of rows deleted.
212
+ */
213
+ export async function deleteBatch(opts: DeleteBatchOptions): Promise<DeleteBatchResult> {
214
+ const { table, userId, filters } = opts
215
+ const dryRun = opts.dryRun ?? true // safe by default
216
+ const supabase = getClientForTable(table)
217
+ const serializedFilters = serializeFilters(table, userId, filters)
218
+
219
+ if (dryRun) {
220
+ // Count matching rows without deleting
221
+ const countQuery = supabase
222
+ .from(table)
223
+ .select('*', { count: 'exact', head: true })
224
+
225
+ const countQueryWithFilters = applyFilters(countQuery, table, userId, filters)
226
+ const { count, error } = await countQueryWithFilters
227
+
228
+ if (error) {
229
+ throw new Error(`deleteBatch dry-run count error on ${table}: ${error.message}`)
230
+ }
231
+
232
+ return {
233
+ table,
234
+ deletedCount: 0,
235
+ dryRun: true,
236
+ filters: serializedFilters,
237
+ }
238
+ }
239
+
240
+ // Execute deletion
241
+ const deleteQuery = supabase
242
+ .from(table)
243
+ .delete({ count: 'exact' })
244
+
245
+ const deleteQueryWithFilters = applyFilters(deleteQuery, table, userId, filters)
246
+ const { count, error } = await deleteQueryWithFilters
247
+
248
+ if (error) {
249
+ throw new Error(`deleteBatch execute error on ${table}: ${error.message}`)
250
+ }
251
+
252
+ return {
253
+ table,
254
+ deletedCount: count ?? 0,
255
+ dryRun: false,
256
+ filters: serializedFilters,
257
+ }
258
+ }