optimal-cli 0.1.0 → 1.0.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 (107) hide show
  1. package/agents/.gitkeep +0 -0
  2. package/agents/content-ops.md +227 -0
  3. package/agents/financial-ops.md +184 -0
  4. package/agents/infra-ops.md +206 -0
  5. package/agents/profiles.json +5 -0
  6. package/bin/optimal.ts +1731 -0
  7. package/docs/CLI-REFERENCE.md +361 -0
  8. package/lib/assets/index.ts +225 -0
  9. package/lib/assets.ts +124 -0
  10. package/lib/auth/index.ts +189 -0
  11. package/lib/board/index.ts +309 -0
  12. package/lib/board/types.ts +124 -0
  13. package/lib/bot/claim.ts +43 -0
  14. package/lib/bot/coordinator.ts +254 -0
  15. package/lib/bot/heartbeat.ts +37 -0
  16. package/lib/bot/index.ts +9 -0
  17. package/lib/bot/protocol.ts +99 -0
  18. package/lib/bot/reporter.ts +42 -0
  19. package/lib/bot/skills.ts +81 -0
  20. package/lib/budget/projections.ts +561 -0
  21. package/lib/budget/scenarios.ts +312 -0
  22. package/lib/cms/publish-blog.ts +129 -0
  23. package/lib/cms/strapi-client.ts +302 -0
  24. package/lib/config/registry.ts +228 -0
  25. package/lib/config/schema.ts +58 -0
  26. package/lib/config.ts +247 -0
  27. package/lib/errors.ts +129 -0
  28. package/lib/format.ts +120 -0
  29. package/lib/infra/.gitkeep +0 -0
  30. package/lib/infra/deploy.ts +70 -0
  31. package/lib/infra/migrate.ts +141 -0
  32. package/lib/newsletter/.gitkeep +0 -0
  33. package/lib/newsletter/distribute.ts +256 -0
  34. package/{dist/lib/newsletter/generate-insurance.d.ts → lib/newsletter/generate-insurance.ts} +24 -7
  35. package/lib/newsletter/generate.ts +735 -0
  36. package/lib/returnpro/.gitkeep +0 -0
  37. package/lib/returnpro/anomalies.ts +258 -0
  38. package/lib/returnpro/audit.ts +194 -0
  39. package/lib/returnpro/diagnose.ts +400 -0
  40. package/lib/returnpro/kpis.ts +255 -0
  41. package/lib/returnpro/templates.ts +323 -0
  42. package/lib/returnpro/upload-income.ts +311 -0
  43. package/lib/returnpro/upload-netsuite.ts +696 -0
  44. package/lib/returnpro/upload-r1.ts +563 -0
  45. package/lib/returnpro/validate.ts +154 -0
  46. package/lib/social/meta.ts +228 -0
  47. package/lib/social/post-generator.ts +468 -0
  48. package/lib/social/publish.ts +301 -0
  49. package/lib/social/scraper.ts +503 -0
  50. package/lib/supabase.ts +25 -0
  51. package/lib/transactions/delete-batch.ts +258 -0
  52. package/lib/transactions/ingest.ts +659 -0
  53. package/lib/transactions/stamp.ts +654 -0
  54. package/package.json +15 -25
  55. package/dist/bin/optimal.d.ts +0 -2
  56. package/dist/bin/optimal.js +0 -995
  57. package/dist/lib/budget/projections.d.ts +0 -115
  58. package/dist/lib/budget/projections.js +0 -384
  59. package/dist/lib/budget/scenarios.d.ts +0 -93
  60. package/dist/lib/budget/scenarios.js +0 -214
  61. package/dist/lib/cms/publish-blog.d.ts +0 -62
  62. package/dist/lib/cms/publish-blog.js +0 -74
  63. package/dist/lib/cms/strapi-client.d.ts +0 -123
  64. package/dist/lib/cms/strapi-client.js +0 -213
  65. package/dist/lib/config.d.ts +0 -55
  66. package/dist/lib/config.js +0 -206
  67. package/dist/lib/infra/deploy.d.ts +0 -29
  68. package/dist/lib/infra/deploy.js +0 -58
  69. package/dist/lib/infra/migrate.d.ts +0 -34
  70. package/dist/lib/infra/migrate.js +0 -103
  71. package/dist/lib/kanban.d.ts +0 -46
  72. package/dist/lib/kanban.js +0 -118
  73. package/dist/lib/newsletter/distribute.d.ts +0 -52
  74. package/dist/lib/newsletter/distribute.js +0 -193
  75. package/dist/lib/newsletter/generate-insurance.js +0 -36
  76. package/dist/lib/newsletter/generate.d.ts +0 -104
  77. package/dist/lib/newsletter/generate.js +0 -571
  78. package/dist/lib/returnpro/anomalies.d.ts +0 -64
  79. package/dist/lib/returnpro/anomalies.js +0 -166
  80. package/dist/lib/returnpro/audit.d.ts +0 -32
  81. package/dist/lib/returnpro/audit.js +0 -147
  82. package/dist/lib/returnpro/diagnose.d.ts +0 -52
  83. package/dist/lib/returnpro/diagnose.js +0 -281
  84. package/dist/lib/returnpro/kpis.d.ts +0 -32
  85. package/dist/lib/returnpro/kpis.js +0 -192
  86. package/dist/lib/returnpro/templates.d.ts +0 -48
  87. package/dist/lib/returnpro/templates.js +0 -229
  88. package/dist/lib/returnpro/upload-income.d.ts +0 -25
  89. package/dist/lib/returnpro/upload-income.js +0 -235
  90. package/dist/lib/returnpro/upload-netsuite.d.ts +0 -37
  91. package/dist/lib/returnpro/upload-netsuite.js +0 -566
  92. package/dist/lib/returnpro/upload-r1.d.ts +0 -48
  93. package/dist/lib/returnpro/upload-r1.js +0 -398
  94. package/dist/lib/social/post-generator.d.ts +0 -83
  95. package/dist/lib/social/post-generator.js +0 -333
  96. package/dist/lib/social/publish.d.ts +0 -66
  97. package/dist/lib/social/publish.js +0 -226
  98. package/dist/lib/social/scraper.d.ts +0 -67
  99. package/dist/lib/social/scraper.js +0 -361
  100. package/dist/lib/supabase.d.ts +0 -4
  101. package/dist/lib/supabase.js +0 -20
  102. package/dist/lib/transactions/delete-batch.d.ts +0 -60
  103. package/dist/lib/transactions/delete-batch.js +0 -203
  104. package/dist/lib/transactions/ingest.d.ts +0 -43
  105. package/dist/lib/transactions/ingest.js +0 -555
  106. package/dist/lib/transactions/stamp.d.ts +0 -51
  107. package/dist/lib/transactions/stamp.js +0 -524
@@ -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
+ }