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.
Files changed (85) 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 +278 -591
  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/config/registry.ts +5 -4
  17. package/lib/kanban-obsidian.ts +232 -0
  18. package/lib/kanban-sync.ts +258 -0
  19. package/lib/kanban.ts +239 -0
  20. package/lib/obsidian-tasks.ts +231 -0
  21. package/package.json +5 -19
  22. package/pnpm-workspace.yaml +3 -0
  23. package/scripts/check-table.ts +24 -0
  24. package/scripts/create-tables.ts +94 -0
  25. package/scripts/migrate-kanban.sh +28 -0
  26. package/scripts/migrate-v2.ts +78 -0
  27. package/scripts/migrate.ts +79 -0
  28. package/scripts/run-migration.ts +59 -0
  29. package/scripts/seed-board.ts +203 -0
  30. package/scripts/test-kanban.ts +21 -0
  31. package/skills/audit-financials/SKILL.md +33 -0
  32. package/skills/board-create/SKILL.md +28 -0
  33. package/skills/board-update/SKILL.md +27 -0
  34. package/skills/board-view/SKILL.md +27 -0
  35. package/skills/delete-batch/SKILL.md +77 -0
  36. package/skills/deploy/SKILL.md +40 -0
  37. package/skills/diagnose-months/SKILL.md +68 -0
  38. package/skills/distribute-newsletter/SKILL.md +58 -0
  39. package/skills/export-budget/SKILL.md +44 -0
  40. package/skills/export-kpis/SKILL.md +52 -0
  41. package/skills/generate-netsuite-template/SKILL.md +51 -0
  42. package/skills/generate-newsletter/SKILL.md +53 -0
  43. package/skills/generate-newsletter-insurance/SKILL.md +59 -0
  44. package/skills/generate-social-posts/SKILL.md +67 -0
  45. package/skills/health-check/SKILL.md +42 -0
  46. package/skills/ingest-transactions/SKILL.md +51 -0
  47. package/skills/manage-cms/SKILL.md +50 -0
  48. package/skills/manage-scenarios/SKILL.md +83 -0
  49. package/skills/migrate-db/SKILL.md +79 -0
  50. package/skills/preview-newsletter/SKILL.md +50 -0
  51. package/skills/project-budget/SKILL.md +60 -0
  52. package/skills/publish-blog/SKILL.md +70 -0
  53. package/skills/publish-social-posts/SKILL.md +70 -0
  54. package/skills/rate-anomalies/SKILL.md +62 -0
  55. package/skills/scrape-ads/SKILL.md +49 -0
  56. package/skills/stamp-transactions/SKILL.md +62 -0
  57. package/skills/upload-income-statements/SKILL.md +54 -0
  58. package/skills/upload-netsuite/SKILL.md +56 -0
  59. package/skills/upload-r1/SKILL.md +45 -0
  60. package/supabase/.temp/cli-latest +1 -0
  61. package/supabase/migrations/.gitkeep +0 -0
  62. package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
  63. package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
  64. package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
  65. package/tests/config-command-smoke.test.ts +395 -0
  66. package/tests/config-registry.test.ts +173 -0
  67. package/tsconfig.json +19 -0
  68. package/agents/profiles.json +0 -5
  69. package/docs/CLI-REFERENCE.md +0 -361
  70. package/lib/assets/index.ts +0 -225
  71. package/lib/assets.ts +0 -124
  72. package/lib/auth/index.ts +0 -189
  73. package/lib/board/index.ts +0 -309
  74. package/lib/board/types.ts +0 -124
  75. package/lib/bot/claim.ts +0 -43
  76. package/lib/bot/coordinator.ts +0 -254
  77. package/lib/bot/heartbeat.ts +0 -37
  78. package/lib/bot/index.ts +0 -9
  79. package/lib/bot/protocol.ts +0 -99
  80. package/lib/bot/reporter.ts +0 -42
  81. package/lib/bot/skills.ts +0 -81
  82. package/lib/errors.ts +0 -129
  83. package/lib/format.ts +0 -120
  84. package/lib/returnpro/validate.ts +0 -154
  85. package/lib/social/meta.ts +0 -228
package/lib/format.ts DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * Lightweight CLI output formatting — ANSI colors, tables, badges.
3
- * Zero external dependencies. Respects NO_COLOR env var.
4
- */
5
-
6
- // ── ANSI escape codes ───────────────────────────────────────────────
7
-
8
- const CODES: Record<string, [number, number]> = {
9
- red: [31, 39],
10
- green: [32, 39],
11
- yellow: [33, 39],
12
- blue: [34, 39],
13
- cyan: [36, 39],
14
- gray: [90, 39],
15
- bold: [1, 22],
16
- dim: [2, 22],
17
- }
18
-
19
- type Color = 'red' | 'green' | 'yellow' | 'blue' | 'cyan' | 'gray' | 'bold' | 'dim'
20
-
21
- /**
22
- * Wrap text in ANSI escape codes for the given color/style.
23
- * Returns plain text when NO_COLOR env var is set.
24
- */
25
- export function colorize(text: string, color: Color): string {
26
- if (process.env.NO_COLOR !== undefined) return text
27
- const [open, close] = CODES[color]
28
- return `\x1b[${open}m${text}\x1b[${close}m`
29
- }
30
-
31
- // ── Table rendering ─────────────────────────────────────────────────
32
-
33
- /** Strip ANSI escape sequences to measure visible string width. */
34
- function stripAnsi(s: string): string {
35
- return s.replace(/\x1b\[\d+m/g, '')
36
- }
37
-
38
- /**
39
- * Render a bordered ASCII table with auto-sized columns.
40
- * Headers are rendered in bold.
41
- */
42
- export function table(headers: string[], rows: string[][]): string {
43
- // Compute column widths from headers and all rows
44
- const colWidths = headers.map((h, i) => {
45
- let max = stripAnsi(h).length
46
- for (const row of rows) {
47
- const cell = row[i] ?? ''
48
- const len = stripAnsi(cell).length
49
- if (len > max) max = len
50
- }
51
- return max
52
- })
53
-
54
- function padCell(cell: string, width: number): string {
55
- const visible = stripAnsi(cell).length
56
- return cell + ' '.repeat(Math.max(0, width - visible))
57
- }
58
-
59
- const sep = '+-' + colWidths.map(w => '-'.repeat(w)).join('-+-') + '-+'
60
- const headerRow = '| ' + headers.map((h, i) => padCell(colorize(h, 'bold'), colWidths[i])).join(' | ') + ' |'
61
-
62
- const bodyRows = rows.map(row =>
63
- '| ' + row.map((cell, i) => padCell(cell ?? '', colWidths[i])).join(' | ') + ' |'
64
- )
65
-
66
- return [sep, headerRow, sep, ...bodyRows, sep].join('\n')
67
- }
68
-
69
- // ── Status & priority badges ────────────────────────────────────────
70
-
71
- const STATUS_COLORS: Record<string, Color> = {
72
- done: 'green',
73
- in_progress: 'blue',
74
- blocked: 'red',
75
- ready: 'cyan',
76
- backlog: 'gray',
77
- cancelled: 'dim',
78
- review: 'yellow',
79
- }
80
-
81
- /** Return a colored status string (e.g. "done" in green). */
82
- export function statusBadge(status: string): string {
83
- const color = STATUS_COLORS[status] ?? 'dim'
84
- return colorize(status, color)
85
- }
86
-
87
- const PRIORITY_COLORS: Record<number, Color> = {
88
- 1: 'red',
89
- 2: 'yellow',
90
- 3: 'blue',
91
- 4: 'gray',
92
- }
93
-
94
- /** Return a colored priority label (e.g. "P1" in red). */
95
- export function priorityBadge(p: number): string {
96
- const color = PRIORITY_COLORS[p] ?? 'gray'
97
- return colorize(`P${p}`, color)
98
- }
99
-
100
- // ── Logging helpers ─────────────────────────────────────────────────
101
-
102
- /** Print a green success message with a check mark prefix. */
103
- export function success(msg: string): void {
104
- console.log(`${colorize('\u2713', 'green')} ${msg}`)
105
- }
106
-
107
- /** Print a red error message with an X prefix. */
108
- export function error(msg: string): void {
109
- console.error(`${colorize('\u2717', 'red')} ${msg}`)
110
- }
111
-
112
- /** Print a yellow warning message with a warning prefix. */
113
- export function warn(msg: string): void {
114
- console.warn(`${colorize('\u26a0', 'yellow')} ${msg}`)
115
- }
116
-
117
- /** Print a blue info message with an info prefix. */
118
- export function info(msg: string): void {
119
- console.log(`${colorize('\u2139', 'blue')} ${msg}`)
120
- }
@@ -1,154 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ReturnPro data validation
3
- // ---------------------------------------------------------------------------
4
- //
5
- // Pre-insert validators for stg_financials_raw and confirmed_income_statements.
6
- // No external dependencies — pure TypeScript, no Supabase calls.
7
- // ---------------------------------------------------------------------------
8
-
9
- // --- Types ---
10
-
11
- export interface ValidationResult {
12
- valid: boolean
13
- errors: string[]
14
- }
15
-
16
- export interface BatchValidationResult {
17
- totalRows: number
18
- validRows: number
19
- invalidRows: number
20
- errors: Array<{ row: number; errors: string[] }>
21
- }
22
-
23
- // --- Constants ---
24
-
25
- /** Required fields for a stg_financials_raw row. */
26
- const FINANCIAL_REQUIRED_FIELDS = ['client', 'program', 'account_code', 'amount', 'period'] as const
27
-
28
- /** Required fields for a confirmed_income_statements row. */
29
- const INCOME_REQUIRED_FIELDS = ['account_id', 'client_id', 'amount', 'period', 'source'] as const
30
-
31
- /** Matches YYYY-MM format (e.g. "2025-04"). */
32
- const PERIOD_REGEX = /^\d{4}-(0[1-9]|1[0-2])$/
33
-
34
- // --- Helpers ---
35
-
36
- /**
37
- * Check whether a value is present (not null, undefined, or empty string).
38
- */
39
- function isPresent(value: unknown): boolean {
40
- if (value === null || value === undefined) return false
41
- if (typeof value === 'string' && value.trim() === '') return false
42
- return true
43
- }
44
-
45
- /**
46
- * Check whether a value can be parsed as a finite number.
47
- * Accepts numbers and numeric strings. Rejects NaN, Infinity, empty strings.
48
- */
49
- function isNumeric(value: unknown): boolean {
50
- if (typeof value === 'number') return isFinite(value)
51
- if (typeof value === 'string') {
52
- const trimmed = value.trim()
53
- if (trimmed === '') return false
54
- const parsed = Number(trimmed)
55
- return isFinite(parsed)
56
- }
57
- return false
58
- }
59
-
60
- // --- Core validators ---
61
-
62
- /**
63
- * Validate a single row destined for `stg_financials_raw`.
64
- *
65
- * Checks:
66
- * - Required fields: client, program, account_code, amount, period
67
- * - `amount` is numeric (the DB column is TEXT, so non-numeric strings are a common bug)
68
- * - `period` matches YYYY-MM format
69
- */
70
- export function validateFinancialRow(row: Record<string, unknown>): ValidationResult {
71
- const errors: string[] = []
72
-
73
- // Check required fields
74
- for (const field of FINANCIAL_REQUIRED_FIELDS) {
75
- if (!isPresent(row[field])) {
76
- errors.push(`Missing required field: ${field}`)
77
- }
78
- }
79
-
80
- // Validate amount is numeric (only if present, to avoid duplicate error)
81
- if (isPresent(row.amount) && !isNumeric(row.amount)) {
82
- errors.push(`amount must be numeric, got: ${JSON.stringify(row.amount)}`)
83
- }
84
-
85
- // Validate period format (only if present)
86
- if (isPresent(row.period)) {
87
- const period = String(row.period).trim()
88
- if (!PERIOD_REGEX.test(period)) {
89
- errors.push(`period must be YYYY-MM format (e.g. "2025-04"), got: "${period}"`)
90
- }
91
- }
92
-
93
- return { valid: errors.length === 0, errors }
94
- }
95
-
96
- /**
97
- * Validate a batch of rows destined for `stg_financials_raw`.
98
- *
99
- * Runs `validateFinancialRow` on each row and aggregates results.
100
- */
101
- export function validateBatch(rows: Record<string, unknown>[]): BatchValidationResult {
102
- const batchErrors: Array<{ row: number; errors: string[] }> = []
103
- let validRows = 0
104
-
105
- for (let i = 0; i < rows.length; i++) {
106
- const result = validateFinancialRow(rows[i])
107
- if (result.valid) {
108
- validRows++
109
- } else {
110
- batchErrors.push({ row: i, errors: result.errors })
111
- }
112
- }
113
-
114
- return {
115
- totalRows: rows.length,
116
- validRows,
117
- invalidRows: rows.length - validRows,
118
- errors: batchErrors,
119
- }
120
- }
121
-
122
- /**
123
- * Validate a single row destined for `confirmed_income_statements`.
124
- *
125
- * Checks:
126
- * - Required fields: account_id, client_id, amount, period, source
127
- * - `amount` is numeric
128
- * - `period` matches YYYY-MM format
129
- */
130
- export function validateIncomeStatementRow(row: Record<string, unknown>): ValidationResult {
131
- const errors: string[] = []
132
-
133
- // Check required fields
134
- for (const field of INCOME_REQUIRED_FIELDS) {
135
- if (!isPresent(row[field])) {
136
- errors.push(`Missing required field: ${field}`)
137
- }
138
- }
139
-
140
- // Validate amount is numeric (only if present)
141
- if (isPresent(row.amount) && !isNumeric(row.amount)) {
142
- errors.push(`amount must be numeric, got: ${JSON.stringify(row.amount)}`)
143
- }
144
-
145
- // Validate period format (only if present)
146
- if (isPresent(row.period)) {
147
- const period = String(row.period).trim()
148
- if (!PERIOD_REGEX.test(period)) {
149
- errors.push(`period must be YYYY-MM format (e.g. "2025-04"), got: "${period}"`)
150
- }
151
- }
152
-
153
- return { valid: errors.length === 0, errors }
154
- }
@@ -1,228 +0,0 @@
1
- /**
2
- * Meta Graph API — Instagram Content Publishing
3
- *
4
- * Direct Instagram publishing via Meta's Content Publishing API.
5
- * Replaces n8n webhook intermediary for IG posts.
6
- *
7
- * Functions:
8
- * publishIgPhoto() — Publish a single image post to Instagram
9
- * publishIgCarousel() — Publish a carousel (multi-image) post to Instagram
10
- * getMetaConfig() — Read Meta credentials from env vars
11
- * getMetaConfigForBrand() — Read brand-specific Meta credentials
12
- */
13
-
14
- // ── Types ────────────────────────────────────────────────────────────
15
-
16
- export interface MetaConfig {
17
- accessToken: string
18
- igAccountId: string
19
- }
20
-
21
- export interface PublishIgResult {
22
- containerId: string
23
- mediaId: string
24
- }
25
-
26
- export interface PublishIgPhotoOptions {
27
- imageUrl: string
28
- caption: string
29
- }
30
-
31
- export interface CarouselItem {
32
- imageUrl: string
33
- }
34
-
35
- export interface PublishIgCarouselOptions {
36
- caption: string
37
- items: CarouselItem[]
38
- }
39
-
40
- export class MetaApiError extends Error {
41
- constructor(
42
- message: string,
43
- public status: number,
44
- public metaError?: { message: string; type?: string; code?: number },
45
- ) {
46
- super(message)
47
- this.name = 'MetaApiError'
48
- }
49
- }
50
-
51
- // ── Config ───────────────────────────────────────────────────────────
52
-
53
- const GRAPH_API_BASE = 'https://graph.facebook.com/v21.0'
54
-
55
- // Injectable fetch for testing
56
- let _fetch: typeof globalThis.fetch = globalThis.fetch
57
-
58
- export function setFetchForTests(fn: typeof globalThis.fetch): void {
59
- _fetch = fn
60
- }
61
-
62
- export function resetFetchForTests(): void {
63
- _fetch = globalThis.fetch
64
- }
65
-
66
- // ── Internal helpers ─────────────────────────────────────────────────
67
-
68
- async function graphPost(
69
- path: string,
70
- body: Record<string, unknown>,
71
- ): Promise<{ id: string }> {
72
- const res = await _fetch(`${GRAPH_API_BASE}${path}`, {
73
- method: 'POST',
74
- headers: { 'Content-Type': 'application/json' },
75
- body: JSON.stringify(body),
76
- })
77
-
78
- const data = await res.json() as Record<string, unknown>
79
-
80
- if (!res.ok) {
81
- const err = data.error as { message?: string; type?: string; code?: number } | undefined
82
- throw new MetaApiError(
83
- err?.message ?? `Meta API ${res.status}: ${res.statusText}`,
84
- res.status,
85
- err ? { message: err.message ?? '', type: err.type, code: err.code } : undefined,
86
- )
87
- }
88
-
89
- return data as { id: string }
90
- }
91
-
92
- // ── Config readers ───────────────────────────────────────────────────
93
-
94
- /**
95
- * Read Meta API credentials from environment variables.
96
- * Requires: META_ACCESS_TOKEN, META_IG_ACCOUNT_ID
97
- */
98
- export function getMetaConfig(): MetaConfig {
99
- const accessToken = process.env.META_ACCESS_TOKEN
100
- const igAccountId = process.env.META_IG_ACCOUNT_ID
101
- if (!accessToken) {
102
- throw new Error(
103
- 'Missing env var: META_ACCESS_TOKEN\n' +
104
- 'Get a long-lived page access token from Meta Business Suite:\n' +
105
- ' https://business.facebook.com/settings/system-users',
106
- )
107
- }
108
- if (!igAccountId) {
109
- throw new Error(
110
- 'Missing env var: META_IG_ACCOUNT_ID\n' +
111
- 'Find your IG Business account ID via Graph API Explorer:\n' +
112
- ' GET /me/accounts → page_id → GET /{page_id}?fields=instagram_business_account',
113
- )
114
- }
115
- return { accessToken, igAccountId }
116
- }
117
-
118
- /**
119
- * Read Meta API credentials for a specific brand.
120
- * Looks for META_IG_ACCOUNT_ID_{BRAND} first, falls back to META_IG_ACCOUNT_ID.
121
- * Brand key is normalized: hyphens become underscores (CRE-11TRUST → CRE_11TRUST).
122
- */
123
- export function getMetaConfigForBrand(brand: string): MetaConfig {
124
- const accessToken = process.env.META_ACCESS_TOKEN
125
- if (!accessToken) {
126
- throw new Error('Missing env var: META_ACCESS_TOKEN')
127
- }
128
-
129
- const envKey = `META_IG_ACCOUNT_ID_${brand.replace(/-/g, '_')}`
130
- const igAccountId = process.env[envKey] ?? process.env.META_IG_ACCOUNT_ID
131
-
132
- if (!igAccountId) {
133
- throw new Error(`Missing env var: ${envKey} or META_IG_ACCOUNT_ID`)
134
- }
135
-
136
- return { accessToken, igAccountId }
137
- }
138
-
139
- // ── Publishing ───────────────────────────────────────────────────────
140
-
141
- /**
142
- * Publish a single photo to Instagram.
143
- *
144
- * Two-step process per Meta Content Publishing API:
145
- * 1. Create media container with image_url + caption
146
- * 2. Publish the container
147
- *
148
- * @example
149
- * const result = await publishIgPhoto(config, {
150
- * imageUrl: 'https://cdn.example.com/photo.jpg',
151
- * caption: 'Check out our latest listing! #realestate',
152
- * })
153
- * console.log(`Published: ${result.mediaId}`)
154
- */
155
- export async function publishIgPhoto(
156
- config: MetaConfig,
157
- opts: PublishIgPhotoOptions,
158
- ): Promise<PublishIgResult> {
159
- // Step 1: Create media container
160
- const container = await graphPost(`/${config.igAccountId}/media`, {
161
- image_url: opts.imageUrl,
162
- caption: opts.caption,
163
- access_token: config.accessToken,
164
- })
165
-
166
- // Step 2: Publish the container
167
- const published = await graphPost(`/${config.igAccountId}/media_publish`, {
168
- creation_id: container.id,
169
- access_token: config.accessToken,
170
- })
171
-
172
- return {
173
- containerId: container.id,
174
- mediaId: published.id,
175
- }
176
- }
177
-
178
- /**
179
- * Publish a carousel (multi-image) post to Instagram.
180
- *
181
- * Three-step process:
182
- * 1. Create individual item containers (is_carousel_item=true)
183
- * 2. Create carousel container referencing all item IDs
184
- * 3. Publish the carousel container
185
- *
186
- * @example
187
- * const result = await publishIgCarousel(config, {
188
- * caption: 'Property tour highlights',
189
- * items: [
190
- * { imageUrl: 'https://cdn.example.com/1.jpg' },
191
- * { imageUrl: 'https://cdn.example.com/2.jpg' },
192
- * ],
193
- * })
194
- */
195
- export async function publishIgCarousel(
196
- config: MetaConfig,
197
- opts: PublishIgCarouselOptions,
198
- ): Promise<PublishIgResult> {
199
- // Step 1: Create individual item containers
200
- const itemIds: string[] = []
201
- for (const item of opts.items) {
202
- const container = await graphPost(`/${config.igAccountId}/media`, {
203
- image_url: item.imageUrl,
204
- is_carousel_item: true,
205
- access_token: config.accessToken,
206
- })
207
- itemIds.push(container.id)
208
- }
209
-
210
- // Step 2: Create carousel container
211
- const carousel = await graphPost(`/${config.igAccountId}/media`, {
212
- media_type: 'CAROUSEL',
213
- children: itemIds,
214
- caption: opts.caption,
215
- access_token: config.accessToken,
216
- })
217
-
218
- // Step 3: Publish
219
- const published = await graphPost(`/${config.igAccountId}/media_publish`, {
220
- creation_id: carousel.id,
221
- access_token: config.accessToken,
222
- })
223
-
224
- return {
225
- containerId: carousel.id,
226
- mediaId: published.id,
227
- }
228
- }