optimal-cli 0.1.0 → 1.0.1

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 (47) 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/dist/bin/optimal.d.ts +1 -1
  7. package/dist/bin/optimal.js +706 -111
  8. package/dist/lib/assets/index.d.ts +79 -0
  9. package/dist/lib/assets/index.js +153 -0
  10. package/dist/lib/assets.d.ts +20 -0
  11. package/dist/lib/assets.js +112 -0
  12. package/dist/lib/auth/index.d.ts +83 -0
  13. package/dist/lib/auth/index.js +146 -0
  14. package/dist/lib/board/index.d.ts +39 -0
  15. package/dist/lib/board/index.js +285 -0
  16. package/dist/lib/board/types.d.ts +111 -0
  17. package/dist/lib/board/types.js +1 -0
  18. package/dist/lib/bot/claim.d.ts +3 -0
  19. package/dist/lib/bot/claim.js +20 -0
  20. package/dist/lib/bot/coordinator.d.ts +27 -0
  21. package/dist/lib/bot/coordinator.js +178 -0
  22. package/dist/lib/bot/heartbeat.d.ts +6 -0
  23. package/dist/lib/bot/heartbeat.js +30 -0
  24. package/dist/lib/bot/index.d.ts +9 -0
  25. package/dist/lib/bot/index.js +6 -0
  26. package/dist/lib/bot/protocol.d.ts +12 -0
  27. package/dist/lib/bot/protocol.js +74 -0
  28. package/dist/lib/bot/reporter.d.ts +3 -0
  29. package/dist/lib/bot/reporter.js +27 -0
  30. package/dist/lib/bot/skills.d.ts +26 -0
  31. package/dist/lib/bot/skills.js +69 -0
  32. package/dist/lib/config/registry.d.ts +17 -0
  33. package/dist/lib/config/registry.js +182 -0
  34. package/dist/lib/config/schema.d.ts +31 -0
  35. package/dist/lib/config/schema.js +25 -0
  36. package/dist/lib/errors.d.ts +25 -0
  37. package/dist/lib/errors.js +91 -0
  38. package/dist/lib/format.d.ts +28 -0
  39. package/dist/lib/format.js +98 -0
  40. package/dist/lib/returnpro/validate.d.ts +37 -0
  41. package/dist/lib/returnpro/validate.js +124 -0
  42. package/dist/lib/social/meta.d.ts +90 -0
  43. package/dist/lib/social/meta.js +160 -0
  44. package/docs/CLI-REFERENCE.md +361 -0
  45. package/package.json +13 -24
  46. package/dist/lib/kanban.d.ts +0 -46
  47. package/dist/lib/kanban.js +0 -118
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Lightweight CLI output formatting — ANSI colors, tables, badges.
3
+ * Zero external dependencies. Respects NO_COLOR env var.
4
+ */
5
+ // ── ANSI escape codes ───────────────────────────────────────────────
6
+ const CODES = {
7
+ red: [31, 39],
8
+ green: [32, 39],
9
+ yellow: [33, 39],
10
+ blue: [34, 39],
11
+ cyan: [36, 39],
12
+ gray: [90, 39],
13
+ bold: [1, 22],
14
+ dim: [2, 22],
15
+ };
16
+ /**
17
+ * Wrap text in ANSI escape codes for the given color/style.
18
+ * Returns plain text when NO_COLOR env var is set.
19
+ */
20
+ export function colorize(text, color) {
21
+ if (process.env.NO_COLOR !== undefined)
22
+ return text;
23
+ const [open, close] = CODES[color];
24
+ return `\x1b[${open}m${text}\x1b[${close}m`;
25
+ }
26
+ // ── Table rendering ─────────────────────────────────────────────────
27
+ /** Strip ANSI escape sequences to measure visible string width. */
28
+ function stripAnsi(s) {
29
+ return s.replace(/\x1b\[\d+m/g, '');
30
+ }
31
+ /**
32
+ * Render a bordered ASCII table with auto-sized columns.
33
+ * Headers are rendered in bold.
34
+ */
35
+ export function table(headers, rows) {
36
+ // Compute column widths from headers and all rows
37
+ const colWidths = headers.map((h, i) => {
38
+ let max = stripAnsi(h).length;
39
+ for (const row of rows) {
40
+ const cell = row[i] ?? '';
41
+ const len = stripAnsi(cell).length;
42
+ if (len > max)
43
+ max = len;
44
+ }
45
+ return max;
46
+ });
47
+ function padCell(cell, width) {
48
+ const visible = stripAnsi(cell).length;
49
+ return cell + ' '.repeat(Math.max(0, width - visible));
50
+ }
51
+ const sep = '+-' + colWidths.map(w => '-'.repeat(w)).join('-+-') + '-+';
52
+ const headerRow = '| ' + headers.map((h, i) => padCell(colorize(h, 'bold'), colWidths[i])).join(' | ') + ' |';
53
+ const bodyRows = rows.map(row => '| ' + row.map((cell, i) => padCell(cell ?? '', colWidths[i])).join(' | ') + ' |');
54
+ return [sep, headerRow, sep, ...bodyRows, sep].join('\n');
55
+ }
56
+ // ── Status & priority badges ────────────────────────────────────────
57
+ const STATUS_COLORS = {
58
+ done: 'green',
59
+ in_progress: 'blue',
60
+ blocked: 'red',
61
+ ready: 'cyan',
62
+ backlog: 'gray',
63
+ cancelled: 'dim',
64
+ review: 'yellow',
65
+ };
66
+ /** Return a colored status string (e.g. "done" in green). */
67
+ export function statusBadge(status) {
68
+ const color = STATUS_COLORS[status] ?? 'dim';
69
+ return colorize(status, color);
70
+ }
71
+ const PRIORITY_COLORS = {
72
+ 1: 'red',
73
+ 2: 'yellow',
74
+ 3: 'blue',
75
+ 4: 'gray',
76
+ };
77
+ /** Return a colored priority label (e.g. "P1" in red). */
78
+ export function priorityBadge(p) {
79
+ const color = PRIORITY_COLORS[p] ?? 'gray';
80
+ return colorize(`P${p}`, color);
81
+ }
82
+ // ── Logging helpers ─────────────────────────────────────────────────
83
+ /** Print a green success message with a check mark prefix. */
84
+ export function success(msg) {
85
+ console.log(`${colorize('\u2713', 'green')} ${msg}`);
86
+ }
87
+ /** Print a red error message with an X prefix. */
88
+ export function error(msg) {
89
+ console.error(`${colorize('\u2717', 'red')} ${msg}`);
90
+ }
91
+ /** Print a yellow warning message with a warning prefix. */
92
+ export function warn(msg) {
93
+ console.warn(`${colorize('\u26a0', 'yellow')} ${msg}`);
94
+ }
95
+ /** Print a blue info message with an info prefix. */
96
+ export function info(msg) {
97
+ console.log(`${colorize('\u2139', 'blue')} ${msg}`);
98
+ }
@@ -0,0 +1,37 @@
1
+ export interface ValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ }
5
+ export interface BatchValidationResult {
6
+ totalRows: number;
7
+ validRows: number;
8
+ invalidRows: number;
9
+ errors: Array<{
10
+ row: number;
11
+ errors: string[];
12
+ }>;
13
+ }
14
+ /**
15
+ * Validate a single row destined for `stg_financials_raw`.
16
+ *
17
+ * Checks:
18
+ * - Required fields: client, program, account_code, amount, period
19
+ * - `amount` is numeric (the DB column is TEXT, so non-numeric strings are a common bug)
20
+ * - `period` matches YYYY-MM format
21
+ */
22
+ export declare function validateFinancialRow(row: Record<string, unknown>): ValidationResult;
23
+ /**
24
+ * Validate a batch of rows destined for `stg_financials_raw`.
25
+ *
26
+ * Runs `validateFinancialRow` on each row and aggregates results.
27
+ */
28
+ export declare function validateBatch(rows: Record<string, unknown>[]): BatchValidationResult;
29
+ /**
30
+ * Validate a single row destined for `confirmed_income_statements`.
31
+ *
32
+ * Checks:
33
+ * - Required fields: account_id, client_id, amount, period, source
34
+ * - `amount` is numeric
35
+ * - `period` matches YYYY-MM format
36
+ */
37
+ export declare function validateIncomeStatementRow(row: Record<string, unknown>): ValidationResult;
@@ -0,0 +1,124 @@
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
+ // --- Constants ---
9
+ /** Required fields for a stg_financials_raw row. */
10
+ const FINANCIAL_REQUIRED_FIELDS = ['client', 'program', 'account_code', 'amount', 'period'];
11
+ /** Required fields for a confirmed_income_statements row. */
12
+ const INCOME_REQUIRED_FIELDS = ['account_id', 'client_id', 'amount', 'period', 'source'];
13
+ /** Matches YYYY-MM format (e.g. "2025-04"). */
14
+ const PERIOD_REGEX = /^\d{4}-(0[1-9]|1[0-2])$/;
15
+ // --- Helpers ---
16
+ /**
17
+ * Check whether a value is present (not null, undefined, or empty string).
18
+ */
19
+ function isPresent(value) {
20
+ if (value === null || value === undefined)
21
+ return false;
22
+ if (typeof value === 'string' && value.trim() === '')
23
+ return false;
24
+ return true;
25
+ }
26
+ /**
27
+ * Check whether a value can be parsed as a finite number.
28
+ * Accepts numbers and numeric strings. Rejects NaN, Infinity, empty strings.
29
+ */
30
+ function isNumeric(value) {
31
+ if (typeof value === 'number')
32
+ return isFinite(value);
33
+ if (typeof value === 'string') {
34
+ const trimmed = value.trim();
35
+ if (trimmed === '')
36
+ return false;
37
+ const parsed = Number(trimmed);
38
+ return isFinite(parsed);
39
+ }
40
+ return false;
41
+ }
42
+ // --- Core validators ---
43
+ /**
44
+ * Validate a single row destined for `stg_financials_raw`.
45
+ *
46
+ * Checks:
47
+ * - Required fields: client, program, account_code, amount, period
48
+ * - `amount` is numeric (the DB column is TEXT, so non-numeric strings are a common bug)
49
+ * - `period` matches YYYY-MM format
50
+ */
51
+ export function validateFinancialRow(row) {
52
+ const errors = [];
53
+ // Check required fields
54
+ for (const field of FINANCIAL_REQUIRED_FIELDS) {
55
+ if (!isPresent(row[field])) {
56
+ errors.push(`Missing required field: ${field}`);
57
+ }
58
+ }
59
+ // Validate amount is numeric (only if present, to avoid duplicate error)
60
+ if (isPresent(row.amount) && !isNumeric(row.amount)) {
61
+ errors.push(`amount must be numeric, got: ${JSON.stringify(row.amount)}`);
62
+ }
63
+ // Validate period format (only if present)
64
+ if (isPresent(row.period)) {
65
+ const period = String(row.period).trim();
66
+ if (!PERIOD_REGEX.test(period)) {
67
+ errors.push(`period must be YYYY-MM format (e.g. "2025-04"), got: "${period}"`);
68
+ }
69
+ }
70
+ return { valid: errors.length === 0, errors };
71
+ }
72
+ /**
73
+ * Validate a batch of rows destined for `stg_financials_raw`.
74
+ *
75
+ * Runs `validateFinancialRow` on each row and aggregates results.
76
+ */
77
+ export function validateBatch(rows) {
78
+ const batchErrors = [];
79
+ let validRows = 0;
80
+ for (let i = 0; i < rows.length; i++) {
81
+ const result = validateFinancialRow(rows[i]);
82
+ if (result.valid) {
83
+ validRows++;
84
+ }
85
+ else {
86
+ batchErrors.push({ row: i, errors: result.errors });
87
+ }
88
+ }
89
+ return {
90
+ totalRows: rows.length,
91
+ validRows,
92
+ invalidRows: rows.length - validRows,
93
+ errors: batchErrors,
94
+ };
95
+ }
96
+ /**
97
+ * Validate a single row destined for `confirmed_income_statements`.
98
+ *
99
+ * Checks:
100
+ * - Required fields: account_id, client_id, amount, period, source
101
+ * - `amount` is numeric
102
+ * - `period` matches YYYY-MM format
103
+ */
104
+ export function validateIncomeStatementRow(row) {
105
+ const errors = [];
106
+ // Check required fields
107
+ for (const field of INCOME_REQUIRED_FIELDS) {
108
+ if (!isPresent(row[field])) {
109
+ errors.push(`Missing required field: ${field}`);
110
+ }
111
+ }
112
+ // Validate amount is numeric (only if present)
113
+ if (isPresent(row.amount) && !isNumeric(row.amount)) {
114
+ errors.push(`amount must be numeric, got: ${JSON.stringify(row.amount)}`);
115
+ }
116
+ // Validate period format (only if present)
117
+ if (isPresent(row.period)) {
118
+ const period = String(row.period).trim();
119
+ if (!PERIOD_REGEX.test(period)) {
120
+ errors.push(`period must be YYYY-MM format (e.g. "2025-04"), got: "${period}"`);
121
+ }
122
+ }
123
+ return { valid: errors.length === 0, errors };
124
+ }
@@ -0,0 +1,90 @@
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
+ export interface MetaConfig {
14
+ accessToken: string;
15
+ igAccountId: string;
16
+ }
17
+ export interface PublishIgResult {
18
+ containerId: string;
19
+ mediaId: string;
20
+ }
21
+ export interface PublishIgPhotoOptions {
22
+ imageUrl: string;
23
+ caption: string;
24
+ }
25
+ export interface CarouselItem {
26
+ imageUrl: string;
27
+ }
28
+ export interface PublishIgCarouselOptions {
29
+ caption: string;
30
+ items: CarouselItem[];
31
+ }
32
+ export declare class MetaApiError extends Error {
33
+ status: number;
34
+ metaError?: {
35
+ message: string;
36
+ type?: string;
37
+ code?: number;
38
+ } | undefined;
39
+ constructor(message: string, status: number, metaError?: {
40
+ message: string;
41
+ type?: string;
42
+ code?: number;
43
+ } | undefined);
44
+ }
45
+ export declare function setFetchForTests(fn: typeof globalThis.fetch): void;
46
+ export declare function resetFetchForTests(): void;
47
+ /**
48
+ * Read Meta API credentials from environment variables.
49
+ * Requires: META_ACCESS_TOKEN, META_IG_ACCOUNT_ID
50
+ */
51
+ export declare function getMetaConfig(): MetaConfig;
52
+ /**
53
+ * Read Meta API credentials for a specific brand.
54
+ * Looks for META_IG_ACCOUNT_ID_{BRAND} first, falls back to META_IG_ACCOUNT_ID.
55
+ * Brand key is normalized: hyphens become underscores (CRE-11TRUST → CRE_11TRUST).
56
+ */
57
+ export declare function getMetaConfigForBrand(brand: string): MetaConfig;
58
+ /**
59
+ * Publish a single photo to Instagram.
60
+ *
61
+ * Two-step process per Meta Content Publishing API:
62
+ * 1. Create media container with image_url + caption
63
+ * 2. Publish the container
64
+ *
65
+ * @example
66
+ * const result = await publishIgPhoto(config, {
67
+ * imageUrl: 'https://cdn.example.com/photo.jpg',
68
+ * caption: 'Check out our latest listing! #realestate',
69
+ * })
70
+ * console.log(`Published: ${result.mediaId}`)
71
+ */
72
+ export declare function publishIgPhoto(config: MetaConfig, opts: PublishIgPhotoOptions): Promise<PublishIgResult>;
73
+ /**
74
+ * Publish a carousel (multi-image) post to Instagram.
75
+ *
76
+ * Three-step process:
77
+ * 1. Create individual item containers (is_carousel_item=true)
78
+ * 2. Create carousel container referencing all item IDs
79
+ * 3. Publish the carousel container
80
+ *
81
+ * @example
82
+ * const result = await publishIgCarousel(config, {
83
+ * caption: 'Property tour highlights',
84
+ * items: [
85
+ * { imageUrl: 'https://cdn.example.com/1.jpg' },
86
+ * { imageUrl: 'https://cdn.example.com/2.jpg' },
87
+ * ],
88
+ * })
89
+ */
90
+ export declare function publishIgCarousel(config: MetaConfig, opts: PublishIgCarouselOptions): Promise<PublishIgResult>;
@@ -0,0 +1,160 @@
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
+ export class MetaApiError extends Error {
14
+ status;
15
+ metaError;
16
+ constructor(message, status, metaError) {
17
+ super(message);
18
+ this.status = status;
19
+ this.metaError = metaError;
20
+ this.name = 'MetaApiError';
21
+ }
22
+ }
23
+ // ── Config ───────────────────────────────────────────────────────────
24
+ const GRAPH_API_BASE = 'https://graph.facebook.com/v21.0';
25
+ // Injectable fetch for testing
26
+ let _fetch = globalThis.fetch;
27
+ export function setFetchForTests(fn) {
28
+ _fetch = fn;
29
+ }
30
+ export function resetFetchForTests() {
31
+ _fetch = globalThis.fetch;
32
+ }
33
+ // ── Internal helpers ─────────────────────────────────────────────────
34
+ async function graphPost(path, body) {
35
+ const res = await _fetch(`${GRAPH_API_BASE}${path}`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify(body),
39
+ });
40
+ const data = await res.json();
41
+ if (!res.ok) {
42
+ const err = data.error;
43
+ throw new MetaApiError(err?.message ?? `Meta API ${res.status}: ${res.statusText}`, res.status, err ? { message: err.message ?? '', type: err.type, code: err.code } : undefined);
44
+ }
45
+ return data;
46
+ }
47
+ // ── Config readers ───────────────────────────────────────────────────
48
+ /**
49
+ * Read Meta API credentials from environment variables.
50
+ * Requires: META_ACCESS_TOKEN, META_IG_ACCOUNT_ID
51
+ */
52
+ export function getMetaConfig() {
53
+ const accessToken = process.env.META_ACCESS_TOKEN;
54
+ const igAccountId = process.env.META_IG_ACCOUNT_ID;
55
+ if (!accessToken) {
56
+ throw new Error('Missing env var: META_ACCESS_TOKEN\n' +
57
+ 'Get a long-lived page access token from Meta Business Suite:\n' +
58
+ ' https://business.facebook.com/settings/system-users');
59
+ }
60
+ if (!igAccountId) {
61
+ throw new Error('Missing env var: META_IG_ACCOUNT_ID\n' +
62
+ 'Find your IG Business account ID via Graph API Explorer:\n' +
63
+ ' GET /me/accounts → page_id → GET /{page_id}?fields=instagram_business_account');
64
+ }
65
+ return { accessToken, igAccountId };
66
+ }
67
+ /**
68
+ * Read Meta API credentials for a specific brand.
69
+ * Looks for META_IG_ACCOUNT_ID_{BRAND} first, falls back to META_IG_ACCOUNT_ID.
70
+ * Brand key is normalized: hyphens become underscores (CRE-11TRUST → CRE_11TRUST).
71
+ */
72
+ export function getMetaConfigForBrand(brand) {
73
+ const accessToken = process.env.META_ACCESS_TOKEN;
74
+ if (!accessToken) {
75
+ throw new Error('Missing env var: META_ACCESS_TOKEN');
76
+ }
77
+ const envKey = `META_IG_ACCOUNT_ID_${brand.replace(/-/g, '_')}`;
78
+ const igAccountId = process.env[envKey] ?? process.env.META_IG_ACCOUNT_ID;
79
+ if (!igAccountId) {
80
+ throw new Error(`Missing env var: ${envKey} or META_IG_ACCOUNT_ID`);
81
+ }
82
+ return { accessToken, igAccountId };
83
+ }
84
+ // ── Publishing ───────────────────────────────────────────────────────
85
+ /**
86
+ * Publish a single photo to Instagram.
87
+ *
88
+ * Two-step process per Meta Content Publishing API:
89
+ * 1. Create media container with image_url + caption
90
+ * 2. Publish the container
91
+ *
92
+ * @example
93
+ * const result = await publishIgPhoto(config, {
94
+ * imageUrl: 'https://cdn.example.com/photo.jpg',
95
+ * caption: 'Check out our latest listing! #realestate',
96
+ * })
97
+ * console.log(`Published: ${result.mediaId}`)
98
+ */
99
+ export async function publishIgPhoto(config, opts) {
100
+ // Step 1: Create media container
101
+ const container = await graphPost(`/${config.igAccountId}/media`, {
102
+ image_url: opts.imageUrl,
103
+ caption: opts.caption,
104
+ access_token: config.accessToken,
105
+ });
106
+ // Step 2: Publish the container
107
+ const published = await graphPost(`/${config.igAccountId}/media_publish`, {
108
+ creation_id: container.id,
109
+ access_token: config.accessToken,
110
+ });
111
+ return {
112
+ containerId: container.id,
113
+ mediaId: published.id,
114
+ };
115
+ }
116
+ /**
117
+ * Publish a carousel (multi-image) post to Instagram.
118
+ *
119
+ * Three-step process:
120
+ * 1. Create individual item containers (is_carousel_item=true)
121
+ * 2. Create carousel container referencing all item IDs
122
+ * 3. Publish the carousel container
123
+ *
124
+ * @example
125
+ * const result = await publishIgCarousel(config, {
126
+ * caption: 'Property tour highlights',
127
+ * items: [
128
+ * { imageUrl: 'https://cdn.example.com/1.jpg' },
129
+ * { imageUrl: 'https://cdn.example.com/2.jpg' },
130
+ * ],
131
+ * })
132
+ */
133
+ export async function publishIgCarousel(config, opts) {
134
+ // Step 1: Create individual item containers
135
+ const itemIds = [];
136
+ for (const item of opts.items) {
137
+ const container = await graphPost(`/${config.igAccountId}/media`, {
138
+ image_url: item.imageUrl,
139
+ is_carousel_item: true,
140
+ access_token: config.accessToken,
141
+ });
142
+ itemIds.push(container.id);
143
+ }
144
+ // Step 2: Create carousel container
145
+ const carousel = await graphPost(`/${config.igAccountId}/media`, {
146
+ media_type: 'CAROUSEL',
147
+ children: itemIds,
148
+ caption: opts.caption,
149
+ access_token: config.accessToken,
150
+ });
151
+ // Step 3: Publish
152
+ const published = await graphPost(`/${config.igAccountId}/media_publish`, {
153
+ creation_id: carousel.id,
154
+ access_token: config.accessToken,
155
+ });
156
+ return {
157
+ containerId: carousel.id,
158
+ mediaId: published.id,
159
+ };
160
+ }