optimal-cli 1.0.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 (135) hide show
  1. package/dist/bin/optimal.d.ts +2 -0
  2. package/dist/bin/optimal.js +1590 -0
  3. package/dist/lib/assets/index.d.ts +79 -0
  4. package/dist/lib/assets/index.js +153 -0
  5. package/dist/lib/assets.d.ts +20 -0
  6. package/dist/lib/assets.js +112 -0
  7. package/dist/lib/auth/index.d.ts +83 -0
  8. package/dist/lib/auth/index.js +146 -0
  9. package/dist/lib/board/index.d.ts +39 -0
  10. package/dist/lib/board/index.js +285 -0
  11. package/dist/lib/board/types.d.ts +111 -0
  12. package/dist/lib/board/types.js +1 -0
  13. package/dist/lib/bot/claim.d.ts +3 -0
  14. package/dist/lib/bot/claim.js +20 -0
  15. package/dist/lib/bot/coordinator.d.ts +27 -0
  16. package/dist/lib/bot/coordinator.js +178 -0
  17. package/dist/lib/bot/heartbeat.d.ts +6 -0
  18. package/dist/lib/bot/heartbeat.js +30 -0
  19. package/dist/lib/bot/index.d.ts +9 -0
  20. package/dist/lib/bot/index.js +6 -0
  21. package/dist/lib/bot/protocol.d.ts +12 -0
  22. package/dist/lib/bot/protocol.js +74 -0
  23. package/dist/lib/bot/reporter.d.ts +3 -0
  24. package/dist/lib/bot/reporter.js +27 -0
  25. package/dist/lib/bot/skills.d.ts +26 -0
  26. package/dist/lib/bot/skills.js +69 -0
  27. package/dist/lib/budget/projections.d.ts +115 -0
  28. package/dist/lib/budget/projections.js +384 -0
  29. package/dist/lib/budget/scenarios.d.ts +93 -0
  30. package/dist/lib/budget/scenarios.js +214 -0
  31. package/dist/lib/cms/publish-blog.d.ts +62 -0
  32. package/dist/lib/cms/publish-blog.js +74 -0
  33. package/dist/lib/cms/strapi-client.d.ts +123 -0
  34. package/dist/lib/cms/strapi-client.js +213 -0
  35. package/dist/lib/config/registry.d.ts +17 -0
  36. package/dist/lib/config/registry.js +182 -0
  37. package/dist/lib/config/schema.d.ts +31 -0
  38. package/dist/lib/config/schema.js +25 -0
  39. package/dist/lib/config.d.ts +55 -0
  40. package/dist/lib/config.js +206 -0
  41. package/dist/lib/errors.d.ts +25 -0
  42. package/dist/lib/errors.js +91 -0
  43. package/dist/lib/format.d.ts +28 -0
  44. package/dist/lib/format.js +98 -0
  45. package/dist/lib/infra/deploy.d.ts +29 -0
  46. package/dist/lib/infra/deploy.js +58 -0
  47. package/dist/lib/infra/migrate.d.ts +34 -0
  48. package/dist/lib/infra/migrate.js +103 -0
  49. package/dist/lib/newsletter/distribute.d.ts +52 -0
  50. package/dist/lib/newsletter/distribute.js +193 -0
  51. package/{lib/newsletter/generate-insurance.ts → dist/lib/newsletter/generate-insurance.d.ts} +7 -24
  52. package/dist/lib/newsletter/generate-insurance.js +36 -0
  53. package/dist/lib/newsletter/generate.d.ts +104 -0
  54. package/dist/lib/newsletter/generate.js +571 -0
  55. package/dist/lib/returnpro/anomalies.d.ts +64 -0
  56. package/dist/lib/returnpro/anomalies.js +166 -0
  57. package/dist/lib/returnpro/audit.d.ts +32 -0
  58. package/dist/lib/returnpro/audit.js +147 -0
  59. package/dist/lib/returnpro/diagnose.d.ts +52 -0
  60. package/dist/lib/returnpro/diagnose.js +281 -0
  61. package/dist/lib/returnpro/kpis.d.ts +32 -0
  62. package/dist/lib/returnpro/kpis.js +192 -0
  63. package/dist/lib/returnpro/templates.d.ts +48 -0
  64. package/dist/lib/returnpro/templates.js +229 -0
  65. package/dist/lib/returnpro/upload-income.d.ts +25 -0
  66. package/dist/lib/returnpro/upload-income.js +235 -0
  67. package/dist/lib/returnpro/upload-netsuite.d.ts +37 -0
  68. package/dist/lib/returnpro/upload-netsuite.js +566 -0
  69. package/dist/lib/returnpro/upload-r1.d.ts +48 -0
  70. package/dist/lib/returnpro/upload-r1.js +398 -0
  71. package/dist/lib/returnpro/validate.d.ts +37 -0
  72. package/dist/lib/returnpro/validate.js +124 -0
  73. package/dist/lib/social/meta.d.ts +90 -0
  74. package/dist/lib/social/meta.js +160 -0
  75. package/dist/lib/social/post-generator.d.ts +83 -0
  76. package/dist/lib/social/post-generator.js +333 -0
  77. package/dist/lib/social/publish.d.ts +66 -0
  78. package/dist/lib/social/publish.js +226 -0
  79. package/dist/lib/social/scraper.d.ts +67 -0
  80. package/dist/lib/social/scraper.js +361 -0
  81. package/dist/lib/supabase.d.ts +4 -0
  82. package/dist/lib/supabase.js +20 -0
  83. package/dist/lib/transactions/delete-batch.d.ts +60 -0
  84. package/dist/lib/transactions/delete-batch.js +203 -0
  85. package/dist/lib/transactions/ingest.d.ts +43 -0
  86. package/dist/lib/transactions/ingest.js +555 -0
  87. package/dist/lib/transactions/stamp.d.ts +51 -0
  88. package/dist/lib/transactions/stamp.js +524 -0
  89. package/package.json +3 -4
  90. package/bin/optimal.ts +0 -1731
  91. package/lib/assets/index.ts +0 -225
  92. package/lib/assets.ts +0 -124
  93. package/lib/auth/index.ts +0 -189
  94. package/lib/board/index.ts +0 -309
  95. package/lib/board/types.ts +0 -124
  96. package/lib/bot/claim.ts +0 -43
  97. package/lib/bot/coordinator.ts +0 -254
  98. package/lib/bot/heartbeat.ts +0 -37
  99. package/lib/bot/index.ts +0 -9
  100. package/lib/bot/protocol.ts +0 -99
  101. package/lib/bot/reporter.ts +0 -42
  102. package/lib/bot/skills.ts +0 -81
  103. package/lib/budget/projections.ts +0 -561
  104. package/lib/budget/scenarios.ts +0 -312
  105. package/lib/cms/publish-blog.ts +0 -129
  106. package/lib/cms/strapi-client.ts +0 -302
  107. package/lib/config/registry.ts +0 -228
  108. package/lib/config/schema.ts +0 -58
  109. package/lib/config.ts +0 -247
  110. package/lib/errors.ts +0 -129
  111. package/lib/format.ts +0 -120
  112. package/lib/infra/.gitkeep +0 -0
  113. package/lib/infra/deploy.ts +0 -70
  114. package/lib/infra/migrate.ts +0 -141
  115. package/lib/newsletter/.gitkeep +0 -0
  116. package/lib/newsletter/distribute.ts +0 -256
  117. package/lib/newsletter/generate.ts +0 -735
  118. package/lib/returnpro/.gitkeep +0 -0
  119. package/lib/returnpro/anomalies.ts +0 -258
  120. package/lib/returnpro/audit.ts +0 -194
  121. package/lib/returnpro/diagnose.ts +0 -400
  122. package/lib/returnpro/kpis.ts +0 -255
  123. package/lib/returnpro/templates.ts +0 -323
  124. package/lib/returnpro/upload-income.ts +0 -311
  125. package/lib/returnpro/upload-netsuite.ts +0 -696
  126. package/lib/returnpro/upload-r1.ts +0 -563
  127. package/lib/returnpro/validate.ts +0 -154
  128. package/lib/social/meta.ts +0 -228
  129. package/lib/social/post-generator.ts +0 -468
  130. package/lib/social/publish.ts +0 -301
  131. package/lib/social/scraper.ts +0 -503
  132. package/lib/supabase.ts +0 -25
  133. package/lib/transactions/delete-batch.ts +0 -258
  134. package/lib/transactions/ingest.ts +0 -659
  135. package/lib/transactions/stamp.ts +0 -654
@@ -0,0 +1,1590 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import 'dotenv/config';
4
+ import { createProject, getProjectBySlug, listProjects, updateProject, createMilestone, listMilestones, createLabel, listLabels, createTask, updateTask, listTasks, claimTask, addComment, listActivity, formatBoardTable, } from '../lib/board/index.js';
5
+ import { runAuditComparison } from '../lib/returnpro/audit.js';
6
+ import { exportKpis, formatKpiTable, formatKpiCsv } from '../lib/returnpro/kpis.js';
7
+ import { deploy, healthCheck, listApps } from '../lib/infra/deploy.js';
8
+ import { fetchWesImports, parseSummaryFromJson, initializeProjections, applyUniformAdjustment, calculateTotals, exportToCSV, formatProjectionTable, } from '../lib/budget/projections.js';
9
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
10
+ import { generateNewsletter } from '../lib/newsletter/generate.js';
11
+ import { scrapeCompanies, formatCsv } from '../lib/social/scraper.js';
12
+ import { ingestTransactions } from '../lib/transactions/ingest.js';
13
+ import { stampTransactions } from '../lib/transactions/stamp.js';
14
+ import { processR1Upload } from '../lib/returnpro/upload-r1.js';
15
+ import { processNetSuiteUpload } from '../lib/returnpro/upload-netsuite.js';
16
+ import { uploadIncomeStatements } from '../lib/returnpro/upload-income.js';
17
+ import { detectRateAnomalies } from '../lib/returnpro/anomalies.js';
18
+ import { diagnoseMonths } from '../lib/returnpro/diagnose.js';
19
+ import { generateNetSuiteTemplate } from '../lib/returnpro/templates.js';
20
+ import { distributeNewsletter, checkDistributionStatus } from '../lib/newsletter/distribute.js';
21
+ import { generateSocialPosts } from '../lib/social/post-generator.js';
22
+ import { publishSocialPosts, getPublishQueue, retryFailed } from '../lib/social/publish.js';
23
+ import { publishBlog, listBlogDrafts } from '../lib/cms/publish-blog.js';
24
+ import { publishIgPhoto, getMetaConfigForBrand } from '../lib/social/meta.js';
25
+ import { strapiGet, strapiPut } from '../lib/cms/strapi-client.js';
26
+ import { migrateDb, listPendingMigrations, createMigration } from '../lib/infra/migrate.js';
27
+ import { saveScenario, listScenarios, compareScenarios, deleteScenario } from '../lib/budget/scenarios.js';
28
+ import { deleteBatch, previewBatch } from '../lib/transactions/delete-batch.js';
29
+ import { assertOptimalConfigV1 } from '../lib/config/schema.js';
30
+ import { appendHistory, getHistoryPath, getLocalConfigPath, hashConfig, pullRegistryProfile, pushRegistryProfile, readLocalConfig, writeLocalConfig, } from '../lib/config/registry.js';
31
+ import { sendHeartbeat, getActiveAgents, claimNextTask, releaseTask, reportProgress, reportCompletion, reportBlocked, runCoordinatorLoop, getCoordinatorStatus, assignTask, rebalance, } from '../lib/bot/index.js';
32
+ import { colorize, statusBadge, priorityBadge, success, error as fmtError, warn as fmtWarn, info as fmtInfo, } from '../lib/format.js';
33
+ import { listAssets, createAsset, updateAsset, getAsset, deleteAsset, trackAssetUsage, listAssetUsage, formatAssetTable, } from '../lib/assets/index.js';
34
+ const program = new Command()
35
+ .name('optimal')
36
+ .description('Optimal CLI — unified skills for financial analytics, content, and infra')
37
+ .version('0.1.0')
38
+ .addHelpText('after', `
39
+ Examples:
40
+ $ optimal board view View the kanban board
41
+ $ optimal board view -s in_progress Filter board by status
42
+ $ optimal board claim --id <uuid> --agent bot1 Claim a task
43
+ $ optimal project list List all projects
44
+ $ optimal publish-instagram --brand CRE-11TRUST Publish to Instagram
45
+ $ optimal social-queue --brand CRE-11TRUST View social post queue
46
+ $ optimal generate-newsletter --brand CRE-11TRUST Generate a newsletter
47
+ $ optimal audit-financials --months 2025-01 Audit a single month
48
+ $ optimal export-kpis --format csv > kpis.csv Export KPIs as CSV
49
+ $ optimal deploy dashboard --prod Deploy to production
50
+ $ optimal bot agents List active bot agents
51
+ $ optimal config doctor Validate local config
52
+ `);
53
+ // --- Board commands ---
54
+ const board = program.command('board').description('Kanban board operations')
55
+ .addHelpText('after', `
56
+ Examples:
57
+ $ optimal board view Show full board
58
+ $ optimal board view -p cli-consolidation Filter by project
59
+ $ optimal board view -s ready --mine bot1 Show bot1's ready tasks
60
+ $ optimal board create -t "Fix bug" -p cli-consolidation
61
+ $ optimal board claim --id <uuid> --agent bot1
62
+ $ optimal board log --actor bot1 --limit 5
63
+ `);
64
+ board
65
+ .command('view')
66
+ .description('Display the kanban board')
67
+ .option('-p, --project <slug>', 'Project slug')
68
+ .option('-s, --status <status>', 'Filter by status')
69
+ .option('--mine <agent>', 'Show only tasks claimed by agent')
70
+ .action(async (opts) => {
71
+ const filters = {};
72
+ if (opts.project) {
73
+ const proj = await getProjectBySlug(opts.project);
74
+ filters.project_id = proj.id;
75
+ }
76
+ if (opts.status)
77
+ filters.status = opts.status;
78
+ if (opts.mine)
79
+ filters.claimed_by = opts.mine;
80
+ const tasks = await listTasks(filters);
81
+ console.log(formatBoardTable(tasks));
82
+ });
83
+ board
84
+ .command('create')
85
+ .description('Create a new task')
86
+ .addHelpText('after', `
87
+ Example:
88
+ $ optimal board create -t "Migrate auth" -p cli-consolidation --priority 1 --labels infra,migration
89
+ `)
90
+ .requiredOption('-t, --title <title>', 'Task title')
91
+ .requiredOption('-p, --project <slug>', 'Project slug')
92
+ .option('-d, --description <desc>', 'Task description')
93
+ .option('--priority <n>', 'Priority 1-4', '3')
94
+ .option('--skill <ref>', 'Skill reference')
95
+ .option('--source <repo>', 'Source repo')
96
+ .option('--target <module>', 'Target module')
97
+ .option('--effort <size>', 'Effort: xs, s, m, l, xl')
98
+ .option('--blocked-by <ids>', 'Comma-separated blocking task IDs')
99
+ .option('--labels <labels>', 'Comma-separated labels')
100
+ .action(async (opts) => {
101
+ const project = await getProjectBySlug(opts.project);
102
+ const task = await createTask({
103
+ project_id: project.id,
104
+ title: opts.title,
105
+ description: opts.description,
106
+ priority: parseInt(opts.priority),
107
+ skill_required: opts.skill,
108
+ source_repo: opts.source,
109
+ target_module: opts.target,
110
+ estimated_effort: opts.effort,
111
+ blocked_by: opts.blockedBy?.split(',') ?? [],
112
+ labels: opts.labels?.split(',') ?? [],
113
+ });
114
+ success(`Created task: ${colorize(task.id, 'dim')}\n ${task.title} [${statusBadge(task.status)}] ${priorityBadge(task.priority)}`);
115
+ });
116
+ board
117
+ .command('update')
118
+ .description('Update a task')
119
+ .requiredOption('--id <uuid>', 'Task ID')
120
+ .option('-s, --status <status>', 'New status')
121
+ .option('-a, --agent <name>', 'Assign to agent')
122
+ .option('--priority <n>', 'New priority')
123
+ .option('-m, --message <msg>', 'Log message (adds comment)')
124
+ .action(async (opts) => {
125
+ const updates = {};
126
+ if (opts.status)
127
+ updates.status = opts.status;
128
+ if (opts.agent)
129
+ updates.assigned_to = opts.agent;
130
+ if (opts.priority)
131
+ updates.priority = parseInt(opts.priority);
132
+ if (opts.status === 'done')
133
+ updates.completed_at = new Date().toISOString();
134
+ const task = await updateTask(opts.id, updates, opts.agent ?? 'cli');
135
+ if (opts.message)
136
+ await addComment({ task_id: task.id, author: opts.agent ?? 'cli', body: opts.message });
137
+ success(`Updated: ${task.title} -> ${statusBadge(task.status)}`);
138
+ });
139
+ board
140
+ .command('claim')
141
+ .description('Claim a task (bot pull model)')
142
+ .requiredOption('--id <uuid>', 'Task ID')
143
+ .requiredOption('--agent <name>', 'Agent name')
144
+ .action(async (opts) => {
145
+ const task = await claimTask(opts.id, opts.agent);
146
+ success(`Claimed: ${colorize(task.title, 'cyan')} by ${colorize(opts.agent, 'bold')}`);
147
+ });
148
+ board
149
+ .command('comment')
150
+ .description('Add a comment to a task')
151
+ .requiredOption('--id <uuid>', 'Task ID')
152
+ .requiredOption('--author <name>', 'Author name')
153
+ .requiredOption('--body <text>', 'Comment body')
154
+ .action(async (opts) => {
155
+ const comment = await addComment({ task_id: opts.id, author: opts.author, body: opts.body });
156
+ success(`Comment added by ${colorize(comment.author, 'bold')} at ${colorize(comment.created_at, 'dim')}`);
157
+ });
158
+ board
159
+ .command('log')
160
+ .description('View activity log')
161
+ .option('--task <uuid>', 'Filter by task ID')
162
+ .option('--actor <name>', 'Filter by actor')
163
+ .option('--limit <n>', 'Max entries', '20')
164
+ .action(async (opts) => {
165
+ const entries = await listActivity({
166
+ task_id: opts.task,
167
+ actor: opts.actor,
168
+ limit: parseInt(opts.limit),
169
+ });
170
+ for (const e of entries) {
171
+ console.log(`${e.created_at} | ${e.actor.padEnd(8)} | ${e.action.padEnd(15)} | ${JSON.stringify(e.new_value ?? {})}`);
172
+ }
173
+ console.log(`\n${entries.length} entries`);
174
+ });
175
+ // --- Project commands ---
176
+ const proj = program.command('project').description('Project management')
177
+ .addHelpText('after', `
178
+ Examples:
179
+ $ optimal project list
180
+ $ optimal project create --slug my-proj --name "My Project" --priority 1
181
+ $ optimal project update --slug my-proj -s active
182
+ `);
183
+ proj
184
+ .command('list')
185
+ .description('List all projects')
186
+ .action(async () => {
187
+ const projects = await listProjects();
188
+ console.log('| Status | P | Slug | Owner | Name |');
189
+ console.log('|----------|---|-------------------------|---------|------|');
190
+ for (const p of projects) {
191
+ console.log(`| ${p.status.padEnd(8)} | ${p.priority} | ${p.slug.padEnd(23)} | ${(p.owner ?? '—').padEnd(7)} | ${p.name} |`);
192
+ }
193
+ });
194
+ proj
195
+ .command('create')
196
+ .description('Create a project')
197
+ .requiredOption('--slug <slug>', 'Project slug')
198
+ .requiredOption('--name <name>', 'Project name')
199
+ .option('--owner <name>', 'Owner')
200
+ .option('--priority <n>', 'Priority 1-4', '3')
201
+ .action(async (opts) => {
202
+ const p = await createProject({
203
+ slug: opts.slug,
204
+ name: opts.name,
205
+ owner: opts.owner,
206
+ priority: parseInt(opts.priority),
207
+ });
208
+ success(`Created project: ${colorize(p.slug, 'cyan')} (${colorize(p.id, 'dim')})`);
209
+ });
210
+ proj
211
+ .command('update')
212
+ .description('Update a project')
213
+ .requiredOption('--slug <slug>', 'Project slug')
214
+ .option('-s, --status <status>', 'New status')
215
+ .option('--owner <name>', 'New owner')
216
+ .action(async (opts) => {
217
+ const updates = {};
218
+ if (opts.status)
219
+ updates.status = opts.status;
220
+ if (opts.owner)
221
+ updates.owner = opts.owner;
222
+ const p = await updateProject(opts.slug, updates);
223
+ success(`Updated project: ${colorize(p.slug, 'cyan')} -> ${statusBadge(p.status)}`);
224
+ });
225
+ // --- Milestone commands ---
226
+ const ms = program.command('milestone').description('Milestone management')
227
+ .addHelpText('after', `
228
+ Examples:
229
+ $ optimal milestone list --project cli-consolidation
230
+ $ optimal milestone create --project cli-consolidation --name "v1.0" --due 2026-04-01
231
+ `);
232
+ ms
233
+ .command('create')
234
+ .description('Create a milestone')
235
+ .requiredOption('--project <slug>', 'Project slug')
236
+ .requiredOption('--name <name>', 'Milestone name')
237
+ .option('--due <date>', 'Due date (YYYY-MM-DD)')
238
+ .action(async (opts) => {
239
+ const project = await getProjectBySlug(opts.project);
240
+ const m = await createMilestone({ project_id: project.id, name: opts.name, due_date: opts.due });
241
+ success(`Created milestone: ${colorize(m.name, 'cyan')} (${colorize(m.id, 'dim')})`);
242
+ });
243
+ ms
244
+ .command('list')
245
+ .description('List milestones')
246
+ .option('--project <slug>', 'Filter by project')
247
+ .action(async (opts) => {
248
+ let projectId;
249
+ if (opts.project) {
250
+ const p = await getProjectBySlug(opts.project);
251
+ projectId = p.id;
252
+ }
253
+ const milestones = await listMilestones(projectId);
254
+ for (const m of milestones) {
255
+ console.log(`${m.status.padEnd(10)} | ${m.due_date ?? 'no date'} | ${m.name}`);
256
+ }
257
+ });
258
+ // --- Label commands ---
259
+ const lbl = program.command('label').description('Label management');
260
+ lbl
261
+ .command('create')
262
+ .description('Create a label')
263
+ .requiredOption('--name <name>', 'Label name')
264
+ .option('--color <hex>', 'Color hex code')
265
+ .action(async (opts) => {
266
+ const l = await createLabel(opts.name, opts.color);
267
+ success(`Created label: ${colorize(l.name, 'cyan')} (${colorize(l.id, 'dim')})`);
268
+ });
269
+ lbl
270
+ .command('list')
271
+ .description('List all labels')
272
+ .action(async () => {
273
+ const labels = await listLabels();
274
+ for (const l of labels)
275
+ console.log(`${l.name}${l.color ? ` (${l.color})` : ''}`);
276
+ });
277
+ // Audit financials command
278
+ program
279
+ .command('audit-financials')
280
+ .description('Compare staged financials against confirmed income statements')
281
+ .option('--months <csv>', 'Comma-separated YYYY-MM months to audit (default: all)')
282
+ .option('--tolerance <n>', 'Dollar tolerance for match detection', '1.00')
283
+ .action(async (opts) => {
284
+ const months = opts.months
285
+ ? opts.months.split(',').map((m) => m.trim())
286
+ : undefined;
287
+ const tolerance = parseFloat(opts.tolerance);
288
+ console.log('Fetching financial data...');
289
+ const result = await runAuditComparison(months, tolerance);
290
+ console.log(`\nStaging rows: ${result.totalStagingRows} | Confirmed rows: ${result.totalConfirmedRows}`);
291
+ console.log(`Tolerance: $${tolerance.toFixed(2)}\n`);
292
+ // Header
293
+ console.log('| Month | Confirmed | Staged | Match | SignFlip | Mismatch | C-Only | S-Only | Accuracy |');
294
+ console.log('|---------|-----------|--------|-------|---------|----------|--------|--------|----------|');
295
+ let flagged = false;
296
+ for (const s of result.summaries) {
297
+ const acc = s.accuracy !== null ? `${s.accuracy}%` : 'N/A';
298
+ const warn = s.accuracy !== null && s.accuracy < 100 ? ' *' : '';
299
+ if (warn)
300
+ flagged = true;
301
+ console.log(`| ${s.month} | ${String(s.confirmedAccounts).padStart(9)} | ${String(s.stagedAccounts).padStart(6)} | ${String(s.exactMatch).padStart(5)} | ${String(s.signFlipMatch).padStart(7)} | ${String(s.mismatch).padStart(8)} | ${String(s.confirmedOnly).padStart(6)} | ${String(s.stagingOnly).padStart(6)} | ${(acc + warn).padStart(8)} |`);
302
+ }
303
+ if (flagged) {
304
+ console.log('\n* Months below 100% accuracy — investigate mismatches');
305
+ }
306
+ // Totals row
307
+ if (result.summaries.length > 1) {
308
+ const totals = result.summaries.reduce((acc, s) => ({
309
+ confirmed: acc.confirmed + s.confirmedAccounts,
310
+ staged: acc.staged + s.stagedAccounts,
311
+ exact: acc.exact + s.exactMatch,
312
+ flip: acc.flip + s.signFlipMatch,
313
+ mismatch: acc.mismatch + s.mismatch,
314
+ cOnly: acc.cOnly + s.confirmedOnly,
315
+ sOnly: acc.sOnly + s.stagingOnly,
316
+ }), { confirmed: 0, staged: 0, exact: 0, flip: 0, mismatch: 0, cOnly: 0, sOnly: 0 });
317
+ const totalOverlap = totals.exact + totals.flip + totals.mismatch;
318
+ const totalAcc = totalOverlap > 0
319
+ ? Math.round(((totals.exact + totals.flip) / totalOverlap) * 1000) / 10
320
+ : null;
321
+ console.log(`| TOTAL | ${String(totals.confirmed).padStart(9)} | ${String(totals.staged).padStart(6)} | ${String(totals.exact).padStart(5)} | ${String(totals.flip).padStart(7)} | ${String(totals.mismatch).padStart(8)} | ${String(totals.cOnly).padStart(6)} | ${String(totals.sOnly).padStart(6)} | ${(totalAcc !== null ? `${totalAcc}%` : 'N/A').padStart(8)} |`);
322
+ }
323
+ });
324
+ // Export KPIs command
325
+ program
326
+ .command('export-kpis')
327
+ .description('Export KPI totals by program/client from ReturnPro financial data')
328
+ .option('--months <csv>', 'Comma-separated YYYY-MM months (default: 3 most recent)')
329
+ .option('--programs <csv>', 'Comma-separated program name substrings to filter')
330
+ .option('--format <fmt>', 'Output format: table or csv', 'table')
331
+ .action(async (opts) => {
332
+ const months = opts.months
333
+ ? opts.months.split(',').map((m) => m.trim())
334
+ : undefined;
335
+ const programs = opts.programs
336
+ ? opts.programs.split(',').map((p) => p.trim())
337
+ : undefined;
338
+ const format = opts.format;
339
+ if (format !== 'table' && format !== 'csv') {
340
+ console.error(`Invalid format "${format}". Use "table" or "csv".`);
341
+ process.exit(1);
342
+ }
343
+ console.error('Fetching KPI data...');
344
+ const rows = await exportKpis({ months, programs });
345
+ console.error(`Fetched ${rows.length} KPI rows`);
346
+ if (format === 'csv') {
347
+ console.log(formatKpiCsv(rows));
348
+ }
349
+ else {
350
+ console.log(formatKpiTable(rows));
351
+ }
352
+ });
353
+ // Deploy command
354
+ program
355
+ .command('deploy')
356
+ .description('Deploy an app to Vercel (preview or production)')
357
+ .argument('<app>', `App to deploy (${listApps().join(', ')})`)
358
+ .option('--prod', 'Deploy to production', false)
359
+ .action(async (app, opts) => {
360
+ fmtInfo(`Deploying ${colorize(app, 'cyan')}${opts.prod ? colorize(' (production)', 'yellow') : ' (preview)'}...`);
361
+ try {
362
+ const url = await deploy(app, opts.prod);
363
+ success(`Deployed: ${colorize(url, 'green')}`);
364
+ }
365
+ catch (err) {
366
+ const msg = err instanceof Error ? err.message : String(err);
367
+ fmtError(`Deploy failed: ${msg}`);
368
+ process.exit(1);
369
+ }
370
+ });
371
+ // Health check command
372
+ program
373
+ .command('health-check')
374
+ .description('Run health check across all Optimal services')
375
+ .action(async () => {
376
+ try {
377
+ const output = await healthCheck();
378
+ console.log(output);
379
+ }
380
+ catch (err) {
381
+ const msg = err instanceof Error ? err.message : String(err);
382
+ console.error(`Health check failed: ${msg}`);
383
+ process.exit(1);
384
+ }
385
+ });
386
+ // Budget projection commands
387
+ async function loadProjectionData(opts) {
388
+ if (opts.file) {
389
+ const raw = readFileSync(opts.file, 'utf-8');
390
+ return parseSummaryFromJson(raw);
391
+ }
392
+ const fy = opts.fiscalYear ? parseInt(opts.fiscalYear) : 2025;
393
+ return fetchWesImports({ fiscalYear: fy, userId: opts.userId });
394
+ }
395
+ function resolveAdjustmentType(raw) {
396
+ if (raw === 'flat')
397
+ return 'flat';
398
+ return 'percentage';
399
+ }
400
+ program
401
+ .command('project-budget')
402
+ .description('Run FY26 budget projections with adjustments on FY25 checked-in units')
403
+ .option('--adjustment-type <type>', 'Adjustment type: percent or flat', 'percent')
404
+ .option('--adjustment-value <n>', 'Adjustment value (e.g., 4 for 4%)', '0')
405
+ .option('--format <fmt>', 'Output format: table or csv', 'table')
406
+ .option('--fiscal-year <fy>', 'Base fiscal year for actuals', '2025')
407
+ .option('--user-id <uuid>', 'Supabase user UUID to filter by')
408
+ .option('--file <path>', 'JSON file of CheckedInUnitsSummary[] (skips Supabase)')
409
+ .action(async (opts) => {
410
+ const format = opts.format;
411
+ if (format !== 'table' && format !== 'csv') {
412
+ console.error(`Invalid format "${format}". Use "table" or "csv".`);
413
+ process.exit(1);
414
+ }
415
+ console.error('Loading projection data...');
416
+ const summary = await loadProjectionData(opts);
417
+ console.error(`Loaded ${summary.length} programs`);
418
+ let projections = initializeProjections(summary);
419
+ const adjType = resolveAdjustmentType(opts.adjustmentType);
420
+ const adjValue = parseFloat(opts.adjustmentValue);
421
+ if (adjValue !== 0) {
422
+ projections = applyUniformAdjustment(projections, adjType, adjValue);
423
+ console.error(`Applied ${adjType} adjustment: ${adjType === 'percentage' ? `${adjValue}%` : `${adjValue >= 0 ? '+' : ''}${adjValue} units`}`);
424
+ }
425
+ const totals = calculateTotals(projections);
426
+ console.error(`Totals: ${totals.totalActual} actual -> ${totals.totalProjected} projected (${totals.percentageChange >= 0 ? '+' : ''}${totals.percentageChange.toFixed(1)}%)`);
427
+ if (format === 'csv') {
428
+ console.log(exportToCSV(projections));
429
+ }
430
+ else {
431
+ console.log(formatProjectionTable(projections));
432
+ }
433
+ });
434
+ program
435
+ .command('export-budget')
436
+ .description('Export FY26 budget projections as CSV')
437
+ .option('--adjustment-type <type>', 'Adjustment type: percent or flat', 'percent')
438
+ .option('--adjustment-value <n>', 'Adjustment value (e.g., 4 for 4%)', '0')
439
+ .option('--fiscal-year <fy>', 'Base fiscal year for actuals', '2025')
440
+ .option('--user-id <uuid>', 'Supabase user UUID to filter by')
441
+ .option('--file <path>', 'JSON file of CheckedInUnitsSummary[] (skips Supabase)')
442
+ .action(async (opts) => {
443
+ console.error('Loading projection data...');
444
+ const summary = await loadProjectionData(opts);
445
+ console.error(`Loaded ${summary.length} programs`);
446
+ let projections = initializeProjections(summary);
447
+ const adjType = resolveAdjustmentType(opts.adjustmentType);
448
+ const adjValue = parseFloat(opts.adjustmentValue);
449
+ if (adjValue !== 0) {
450
+ projections = applyUniformAdjustment(projections, adjType, adjValue);
451
+ console.error(`Applied ${adjType} adjustment: ${adjType === 'percentage' ? `${adjValue}%` : `${adjValue >= 0 ? '+' : ''}${adjValue} units`}`);
452
+ }
453
+ console.log(exportToCSV(projections));
454
+ });
455
+ // Newsletter generation command
456
+ program
457
+ .command('generate-newsletter')
458
+ .description('Generate a branded newsletter with AI content and push to Strapi CMS')
459
+ .requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
460
+ .option('--date <date>', 'Edition date as YYYY-MM-DD (default: today)')
461
+ .option('--excel <path>', 'Path to Excel file with property listings (CRE-11TRUST only)')
462
+ .option('--dry-run', 'Generate content but do NOT push to Strapi', false)
463
+ .action(async (opts) => {
464
+ try {
465
+ const result = await generateNewsletter({
466
+ brand: opts.brand,
467
+ date: opts.date,
468
+ excelPath: opts.excel,
469
+ dryRun: opts.dryRun,
470
+ });
471
+ if (result.strapiDocumentId) {
472
+ success(`Strapi documentId: ${colorize(result.strapiDocumentId, 'cyan')}`);
473
+ }
474
+ }
475
+ catch (err) {
476
+ const msg = err instanceof Error ? err.message : String(err);
477
+ fmtError(`Newsletter generation failed: ${msg}`);
478
+ process.exit(1);
479
+ }
480
+ });
481
+ // Scrape Meta Ad Library command
482
+ program
483
+ .command('scrape-ads')
484
+ .description('Scrape Meta Ad Library for competitor ad intelligence')
485
+ .requiredOption('--companies <csv-or-file>', 'Comma-separated company names or path to a text file (one per line)')
486
+ .option('--output <path>', 'Save CSV results to file (default: stdout)')
487
+ .option('--batch-size <n>', 'Companies per batch', '6')
488
+ .action(async (opts) => {
489
+ // Parse companies: file path or comma-separated list
490
+ let companies;
491
+ if (existsSync(opts.companies)) {
492
+ const raw = readFileSync(opts.companies, 'utf-8');
493
+ companies = raw
494
+ .split('\n')
495
+ .map((l) => l.trim())
496
+ .filter((l) => l.length > 0 && !l.startsWith('#'));
497
+ }
498
+ else {
499
+ companies = opts.companies
500
+ .split(',')
501
+ .map((c) => c.trim())
502
+ .filter((c) => c.length > 0);
503
+ }
504
+ if (companies.length === 0) {
505
+ console.error('No companies specified');
506
+ process.exit(1);
507
+ }
508
+ const batchSize = parseInt(opts.batchSize);
509
+ if (isNaN(batchSize) || batchSize < 1) {
510
+ console.error('Invalid batch size');
511
+ process.exit(1);
512
+ }
513
+ try {
514
+ const result = await scrapeCompanies({
515
+ companies,
516
+ outputPath: opts.output,
517
+ batchSize,
518
+ });
519
+ // If no output file, write CSV to stdout
520
+ if (!opts.output) {
521
+ process.stdout.write(formatCsv(result.ads));
522
+ }
523
+ }
524
+ catch (err) {
525
+ const msg = err instanceof Error ? err.message : String(err);
526
+ console.error(`Scrape failed: ${msg}`);
527
+ process.exit(1);
528
+ }
529
+ });
530
+ // Ingest transactions command
531
+ program
532
+ .command('ingest-transactions')
533
+ .description('Parse & deduplicate bank CSV files into the transactions table')
534
+ .requiredOption('--file <path>', 'Path to the CSV file')
535
+ .requiredOption('--user-id <uuid>', 'Supabase user UUID')
536
+ .action(async (opts) => {
537
+ if (!existsSync(opts.file)) {
538
+ console.error(`File not found: ${opts.file}`);
539
+ process.exit(1);
540
+ }
541
+ console.log(`Ingesting transactions from: ${opts.file}`);
542
+ try {
543
+ const result = await ingestTransactions(opts.file, opts.userId);
544
+ console.log(`\nFormat detected: ${result.format}`);
545
+ console.log(`Inserted: ${result.inserted} | Skipped (duplicates): ${result.skipped} | Failed: ${result.failed}`);
546
+ if (result.errors.length > 0) {
547
+ console.log(`\nWarnings/Errors (${result.errors.length}):`);
548
+ for (const err of result.errors.slice(0, 20)) {
549
+ console.log(` - ${err}`);
550
+ }
551
+ if (result.errors.length > 20) {
552
+ console.log(` ... and ${result.errors.length - 20} more`);
553
+ }
554
+ }
555
+ }
556
+ catch (err) {
557
+ const msg = err instanceof Error ? err.message : String(err);
558
+ console.error(`Ingest failed: ${msg}`);
559
+ process.exit(1);
560
+ }
561
+ });
562
+ // Stamp transactions command
563
+ program
564
+ .command('stamp-transactions')
565
+ .description('Auto-categorize unclassified transactions using rule-based matching')
566
+ .requiredOption('--user-id <uuid>', 'Supabase user UUID')
567
+ .option('--dry-run', 'Preview matches without writing to database', false)
568
+ .action(async (opts) => {
569
+ console.log(`Stamping transactions for user: ${opts.userId}${opts.dryRun ? ' (DRY RUN)' : ''}`);
570
+ try {
571
+ const result = await stampTransactions(opts.userId, { dryRun: opts.dryRun });
572
+ console.log(`\nTotal unclassified: ${result.total}`);
573
+ console.log(`Stamped: ${result.stamped} | Unmatched: ${result.unmatched}`);
574
+ console.log(`By match type: PATTERN=${result.byMatchType.PATTERN}, LEARNED=${result.byMatchType.LEARNED}, EXACT=${result.byMatchType.EXACT}, FUZZY=${result.byMatchType.FUZZY}, CATEGORY_INFER=${result.byMatchType.CATEGORY_INFER}`);
575
+ if (result.dryRun) {
576
+ console.log('\n(Dry run — no database changes made)');
577
+ }
578
+ }
579
+ catch (err) {
580
+ const msg = err instanceof Error ? err.message : String(err);
581
+ console.error(`Stamp failed: ${msg}`);
582
+ process.exit(1);
583
+ }
584
+ });
585
+ // ── Upload R1 data ──────────────────────────────────────────────────
586
+ program
587
+ .command('upload-r1')
588
+ .description('Upload R1 XLSX file to ReturnPro staging')
589
+ .requiredOption('--file <path>', 'Path to R1 XLSX file')
590
+ .requiredOption('--user-id <uuid>', 'Supabase user UUID')
591
+ .requiredOption('--month <YYYY-MM>', 'Month in YYYY-MM format')
592
+ .action(async (opts) => {
593
+ if (!existsSync(opts.file)) {
594
+ console.error(`File not found: ${opts.file}`);
595
+ process.exit(1);
596
+ }
597
+ try {
598
+ const result = await processR1Upload(opts.file, opts.userId, opts.month);
599
+ console.log(`R1 upload complete: ${result.rowsInserted} rows inserted, ${result.rowsSkipped} skipped (${result.programGroupsFound} program groups)`);
600
+ if (result.warnings.length > 0) {
601
+ console.log(`Warnings: ${result.warnings.slice(0, 10).join(', ')}`);
602
+ }
603
+ }
604
+ catch (err) {
605
+ console.error(`R1 upload failed: ${err instanceof Error ? err.message : String(err)}`);
606
+ process.exit(1);
607
+ }
608
+ });
609
+ // ── Upload NetSuite data ────────────────────────────────────────────
610
+ program
611
+ .command('upload-netsuite')
612
+ .description('Upload NetSuite CSV/XLSX to ReturnPro staging')
613
+ .requiredOption('--file <path>', 'Path to NetSuite file (CSV, XLSX, or XLSM)')
614
+ .requiredOption('--user-id <uuid>', 'Supabase user UUID')
615
+ .action(async (opts) => {
616
+ if (!existsSync(opts.file)) {
617
+ console.error(`File not found: ${opts.file}`);
618
+ process.exit(1);
619
+ }
620
+ try {
621
+ const result = await processNetSuiteUpload(opts.file, opts.userId);
622
+ console.log(`NetSuite upload: ${result.inserted} rows inserted (months: ${result.monthsCovered.join(', ')})`);
623
+ if (result.warnings.length > 0) {
624
+ console.log(`Warnings: ${result.warnings.slice(0, 10).join(', ')}`);
625
+ }
626
+ }
627
+ catch (err) {
628
+ console.error(`NetSuite upload failed: ${err instanceof Error ? err.message : String(err)}`);
629
+ process.exit(1);
630
+ }
631
+ });
632
+ // ── Upload income statements ────────────────────────────────────────
633
+ program
634
+ .command('upload-income-statements')
635
+ .description('Upload confirmed income statement CSV to ReturnPro')
636
+ .requiredOption('--file <path>', 'Path to income statement CSV')
637
+ .requiredOption('--user-id <uuid>', 'Supabase user UUID')
638
+ .action(async (opts) => {
639
+ if (!existsSync(opts.file)) {
640
+ console.error(`File not found: ${opts.file}`);
641
+ process.exit(1);
642
+ }
643
+ try {
644
+ const result = await uploadIncomeStatements(opts.file, opts.userId);
645
+ console.log(`Income statements: ${result.upserted} rows upserted, ${result.skipped} skipped (period: ${result.period})`);
646
+ }
647
+ catch (err) {
648
+ console.error(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
649
+ process.exit(1);
650
+ }
651
+ });
652
+ // ── Rate anomalies ──────────────────────────────────────────────────
653
+ program
654
+ .command('rate-anomalies')
655
+ .description('Detect rate anomalies via z-score analysis on ReturnPro data')
656
+ .option('--from <YYYY-MM>', 'Start month')
657
+ .option('--to <YYYY-MM>', 'End month')
658
+ .option('--threshold <n>', 'Z-score threshold', '2.0')
659
+ .action(async (opts) => {
660
+ try {
661
+ const months = opts.from && opts.to
662
+ ? (() => {
663
+ const result = [];
664
+ const [fy, fm] = opts.from.split('-').map(Number);
665
+ const [ty, tm] = opts.to.split('-').map(Number);
666
+ let y = fy, m = fm;
667
+ while (y < ty || (y === ty && m <= tm)) {
668
+ result.push(`${y}-${String(m).padStart(2, '0')}`);
669
+ m++;
670
+ if (m > 12) {
671
+ m = 1;
672
+ y++;
673
+ }
674
+ }
675
+ return result;
676
+ })()
677
+ : undefined;
678
+ const result = await detectRateAnomalies({
679
+ months,
680
+ threshold: parseFloat(opts.threshold),
681
+ });
682
+ console.log(`Found ${result.anomalies.length} anomalies (threshold: ${opts.threshold}σ)`);
683
+ for (const a of result.anomalies.slice(0, 30)) {
684
+ console.log(` ${a.month} | ${a.program_code ?? a.master_program} | z=${a.zscore.toFixed(2)} | rate=${a.rate_per_unit}`);
685
+ }
686
+ if (result.anomalies.length > 30)
687
+ console.log(` ... and ${result.anomalies.length - 30} more`);
688
+ }
689
+ catch (err) {
690
+ console.error(`Anomaly detection failed: ${err instanceof Error ? err.message : String(err)}`);
691
+ process.exit(1);
692
+ }
693
+ });
694
+ // ── Diagnose months ─────────────────────────────────────────────────
695
+ program
696
+ .command('diagnose-months')
697
+ .description('Run diagnostic checks on staging data for specified months')
698
+ .option('--months <csv>', 'Comma-separated YYYY-MM months (default: all)')
699
+ .action(async (opts) => {
700
+ const months = opts.months?.split(',').map(m => m.trim());
701
+ try {
702
+ const result = await diagnoseMonths(months ? { months } : undefined);
703
+ console.log(`Analysed months: ${result.monthsAnalysed.join(', ')}`);
704
+ console.log(`Total staging rows: ${result.totalRows} (median: ${result.medianRowCount}/month)\n`);
705
+ for (const issue of result.issues) {
706
+ console.log(` ✗ [${issue.kind}] ${issue.month ?? 'global'}: ${issue.message}`);
707
+ }
708
+ if (result.issues.length === 0) {
709
+ console.log(' ✓ No issues found');
710
+ }
711
+ console.log(`\nSummary: ${result.summary.totalIssues} issues found`);
712
+ }
713
+ catch (err) {
714
+ console.error(`Diagnosis failed: ${err instanceof Error ? err.message : String(err)}`);
715
+ process.exit(1);
716
+ }
717
+ });
718
+ // ── Generate NetSuite template ──────────────────────────────────────
719
+ program
720
+ .command('generate-netsuite-template')
721
+ .description('Generate a blank NetSuite XLSX upload template')
722
+ .option('--output <path>', 'Output file path', 'netsuite-template.xlsx')
723
+ .action(async (opts) => {
724
+ try {
725
+ const result = await generateNetSuiteTemplate(opts.output);
726
+ console.log(`Template saved: ${result.outputPath} (${result.accountCount} accounts)`);
727
+ }
728
+ catch (err) {
729
+ console.error(`Template generation failed: ${err instanceof Error ? err.message : String(err)}`);
730
+ process.exit(1);
731
+ }
732
+ });
733
+ // ── Distribute newsletter ───────────────────────────────────────────
734
+ program
735
+ .command('distribute-newsletter')
736
+ .description('Trigger newsletter distribution via n8n webhook')
737
+ .requiredOption('--document-id <id>', 'Strapi newsletter documentId')
738
+ .option('--channel <ch>', 'Distribution channel: email or all', 'all')
739
+ .action(async (opts) => {
740
+ try {
741
+ const result = await distributeNewsletter(opts.documentId, {
742
+ channel: opts.channel,
743
+ });
744
+ if (result.success) {
745
+ console.log(`Distribution triggered for ${opts.documentId} (channel: ${opts.channel})`);
746
+ }
747
+ else {
748
+ console.error(`Distribution failed: ${result.error}`);
749
+ process.exit(1);
750
+ }
751
+ }
752
+ catch (err) {
753
+ console.error(`Distribution failed: ${err instanceof Error ? err.message : String(err)}`);
754
+ process.exit(1);
755
+ }
756
+ });
757
+ // ── Check distribution status ───────────────────────────────────────
758
+ program
759
+ .command('distribution-status')
760
+ .description('Check delivery status of a newsletter')
761
+ .requiredOption('--document-id <id>', 'Strapi newsletter documentId')
762
+ .action(async (opts) => {
763
+ const status = await checkDistributionStatus(opts.documentId);
764
+ console.log(`Status: ${status.delivery_status}`);
765
+ if (status.delivered_at)
766
+ console.log(`Delivered: ${status.delivered_at}`);
767
+ if (status.recipients_count)
768
+ console.log(`Recipients: ${status.recipients_count}`);
769
+ if (status.ghl_campaign_id)
770
+ console.log(`GHL Campaign: ${status.ghl_campaign_id}`);
771
+ });
772
+ // ── Generate social posts ───────────────────────────────────────────
773
+ program
774
+ .command('generate-social-posts')
775
+ .description('Generate AI-powered social media ad posts and push to Strapi')
776
+ .requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
777
+ .option('--count <n>', 'Number of posts to generate', '9')
778
+ .option('--week-of <date>', 'Week start date YYYY-MM-DD (default: next Monday)')
779
+ .option('--dry-run', 'Generate without pushing to Strapi', false)
780
+ .action(async (opts) => {
781
+ try {
782
+ const result = await generateSocialPosts({
783
+ brand: opts.brand,
784
+ count: parseInt(opts.count),
785
+ weekOf: opts.weekOf,
786
+ dryRun: opts.dryRun,
787
+ });
788
+ console.log(`Created ${result.postsCreated} posts for ${result.brand}`);
789
+ for (const p of result.posts) {
790
+ console.log(` ${p.scheduled_date} | ${p.platform} | ${p.headline}`);
791
+ }
792
+ if (result.errors.length > 0) {
793
+ console.log(`\nErrors: ${result.errors.join(', ')}`);
794
+ }
795
+ }
796
+ catch (err) {
797
+ console.error(`Post generation failed: ${err instanceof Error ? err.message : String(err)}`);
798
+ process.exit(1);
799
+ }
800
+ });
801
+ // ── Publish social posts ────────────────────────────────────────────
802
+ program
803
+ .command('publish-social-posts')
804
+ .description('Publish pending social posts to platforms via n8n')
805
+ .requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
806
+ .option('--limit <n>', 'Max posts to publish')
807
+ .option('--dry-run', 'Preview without publishing', false)
808
+ .option('--retry', 'Retry previously failed posts', false)
809
+ .action(async (opts) => {
810
+ try {
811
+ let result;
812
+ if (opts.retry) {
813
+ result = await retryFailed(opts.brand);
814
+ }
815
+ else {
816
+ result = await publishSocialPosts({
817
+ brand: opts.brand,
818
+ limit: opts.limit ? parseInt(opts.limit) : undefined,
819
+ dryRun: opts.dryRun,
820
+ });
821
+ }
822
+ console.log(`Published: ${result.published} | Failed: ${result.failed} | Skipped: ${result.skipped}`);
823
+ for (const d of result.details) {
824
+ console.log(` ${d.status} | ${d.headline}${d.error ? ` — ${d.error}` : ''}`);
825
+ }
826
+ }
827
+ catch (err) {
828
+ console.error(`Publish failed: ${err instanceof Error ? err.message : String(err)}`);
829
+ process.exit(1);
830
+ }
831
+ });
832
+ // ── Social post queue ───────────────────────────────────────────────
833
+ program
834
+ .command('social-queue')
835
+ .description('View pending social posts ready for publishing')
836
+ .requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
837
+ .action(async (opts) => {
838
+ const queue = await getPublishQueue(opts.brand);
839
+ if (queue.length === 0) {
840
+ console.log('No posts in queue');
841
+ return;
842
+ }
843
+ console.log('| Date | Platform | Headline |');
844
+ console.log('|------|----------|----------|');
845
+ for (const p of queue) {
846
+ console.log(`| ${p.scheduled_date} | ${p.platform} | ${p.headline} |`);
847
+ }
848
+ console.log(`\n${queue.length} posts queued`);
849
+ });
850
+ // ── Publish to Instagram via Meta Graph API ─────────────────────────
851
+ program
852
+ .command('publish-instagram')
853
+ .description('Publish pending social posts to Instagram via Meta Graph API')
854
+ .requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
855
+ .option('--limit <n>', 'Max posts to publish')
856
+ .option('--dry-run', 'Preview without publishing', false)
857
+ .action(async (opts) => {
858
+ try {
859
+ const config = getMetaConfigForBrand(opts.brand);
860
+ // Fetch pending instagram posts from Strapi
861
+ const result = await strapiGet('/api/social-posts', {
862
+ 'filters[brand][$eq]': opts.brand,
863
+ 'filters[delivery_status][$eq]': 'pending',
864
+ 'filters[platform][$eq]': 'instagram',
865
+ 'sort': 'scheduled_date:asc',
866
+ 'pagination[pageSize]': opts.limit ?? '50',
867
+ });
868
+ const posts = result.data;
869
+ if (posts.length === 0) {
870
+ console.log('No pending Instagram posts found');
871
+ return;
872
+ }
873
+ console.log(`Found ${posts.length} pending Instagram post(s) for ${opts.brand}`);
874
+ let published = 0;
875
+ let failed = 0;
876
+ for (const post of posts) {
877
+ const headline = post.headline ?? '(no headline)';
878
+ const imageUrl = post.image_url;
879
+ const caption = (post.body ?? post.headline ?? '').trim();
880
+ if (!imageUrl) {
881
+ console.log(` SKIP | ${headline} — no image_url`);
882
+ failed++;
883
+ continue;
884
+ }
885
+ if (opts.dryRun) {
886
+ console.log(` DRY | ${headline}`);
887
+ continue;
888
+ }
889
+ try {
890
+ const igResult = await publishIgPhoto(config, { imageUrl, caption });
891
+ await strapiPut('/api/social-posts', post.documentId, {
892
+ delivery_status: 'delivered',
893
+ platform_post_id: igResult.mediaId,
894
+ });
895
+ console.log(` OK | ${headline} → ${igResult.mediaId}`);
896
+ published++;
897
+ }
898
+ catch (err) {
899
+ const errMsg = err instanceof Error ? err.message : String(err);
900
+ await strapiPut('/api/social-posts', post.documentId, {
901
+ delivery_status: 'failed',
902
+ delivery_errors: [{ timestamp: new Date().toISOString(), error: errMsg }],
903
+ }).catch(() => { });
904
+ console.log(` FAIL | ${headline} — ${errMsg}`);
905
+ failed++;
906
+ }
907
+ }
908
+ console.log(`\nPublished: ${published} | Failed: ${failed}${opts.dryRun ? ' | (dry run)' : ''}`);
909
+ }
910
+ catch (err) {
911
+ console.error(`Instagram publish failed: ${err instanceof Error ? err.message : String(err)}`);
912
+ process.exit(1);
913
+ }
914
+ });
915
+ // ── Publish blog ────────────────────────────────────────────────────
916
+ program
917
+ .command('publish-blog')
918
+ .description('Publish a Strapi blog post and optionally deploy portfolio site')
919
+ .requiredOption('--slug <slug>', 'Blog post slug')
920
+ .option('--deploy', 'Deploy portfolio site after publishing', false)
921
+ .action(async (opts) => {
922
+ try {
923
+ const result = await publishBlog({ slug: opts.slug, deployAfter: opts.deploy });
924
+ console.log(`Published: ${result.slug} (${result.documentId})`);
925
+ if (result.deployUrl)
926
+ console.log(`Deployed: ${result.deployUrl}`);
927
+ }
928
+ catch (err) {
929
+ console.error(`Publish failed: ${err instanceof Error ? err.message : String(err)}`);
930
+ process.exit(1);
931
+ }
932
+ });
933
+ // ── Blog drafts ─────────────────────────────────────────────────────
934
+ program
935
+ .command('blog-drafts')
936
+ .description('List unpublished blog post drafts')
937
+ .option('--site <site>', 'Filter by site (portfolio, insurance)')
938
+ .action(async (opts) => {
939
+ const drafts = await listBlogDrafts(opts.site);
940
+ if (drafts.length === 0) {
941
+ console.log('No drafts found');
942
+ return;
943
+ }
944
+ console.log('| Created | Site | Title | Slug |');
945
+ console.log('|---------|------|-------|------|');
946
+ for (const d of drafts) {
947
+ console.log(`| ${d.createdAt.slice(0, 10)} | ${d.site} | ${d.title} | ${d.slug} |`);
948
+ }
949
+ });
950
+ // ── Database migration ──────────────────────────────────────────────
951
+ const migrate = program.command('migrate').description('Supabase database migration operations')
952
+ .addHelpText('after', `
953
+ Examples:
954
+ $ optimal migrate pending --target optimalos
955
+ $ optimal migrate push --target returnpro --dry-run
956
+ $ optimal migrate create --target optimalos --name "add-index"
957
+ `);
958
+ migrate
959
+ .command('push')
960
+ .description('Run supabase db push --linked on a target project')
961
+ .requiredOption('--target <t>', 'Target: returnpro or optimalos')
962
+ .option('--dry-run', 'Preview without applying', false)
963
+ .action(async (opts) => {
964
+ const target = opts.target;
965
+ if (target !== 'returnpro' && target !== 'optimalos') {
966
+ console.error('Target must be "returnpro" or "optimalos"');
967
+ process.exit(1);
968
+ }
969
+ console.log(`Migrating ${target}${opts.dryRun ? ' (dry run)' : ''}...`);
970
+ const result = await migrateDb({ target, dryRun: opts.dryRun });
971
+ if (result.success) {
972
+ console.log(result.output);
973
+ }
974
+ else {
975
+ console.error(`Migration failed:\n${result.errors}`);
976
+ process.exit(1);
977
+ }
978
+ });
979
+ migrate
980
+ .command('pending')
981
+ .description('List pending migration files')
982
+ .requiredOption('--target <t>', 'Target: returnpro or optimalos')
983
+ .action(async (opts) => {
984
+ const files = await listPendingMigrations(opts.target);
985
+ if (files.length === 0) {
986
+ console.log('No migration files found');
987
+ return;
988
+ }
989
+ for (const f of files)
990
+ console.log(` ${f}`);
991
+ console.log(`\n${files.length} migration files`);
992
+ });
993
+ migrate
994
+ .command('create')
995
+ .description('Create a new empty migration file')
996
+ .requiredOption('--target <t>', 'Target: returnpro or optimalos')
997
+ .requiredOption('--name <name>', 'Migration name')
998
+ .action(async (opts) => {
999
+ const path = await createMigration(opts.target, opts.name);
1000
+ console.log(`Created: ${path}`);
1001
+ });
1002
+ // ── Budget scenarios ────────────────────────────────────────────────
1003
+ const scenario = program.command('scenario').description('Budget scenario management')
1004
+ .addHelpText('after', `
1005
+ Examples:
1006
+ $ optimal scenario list
1007
+ $ optimal scenario save --name "4pct-growth" --adjustment-type percentage --adjustment-value 4
1008
+ $ optimal scenario compare --names "baseline,4pct-growth"
1009
+ $ optimal scenario delete --name "old-scenario"
1010
+ `);
1011
+ scenario
1012
+ .command('save')
1013
+ .description('Save current projections as a named scenario')
1014
+ .requiredOption('--name <name>', 'Scenario name')
1015
+ .requiredOption('--adjustment-type <type>', 'Adjustment type: percentage or flat')
1016
+ .requiredOption('--adjustment-value <n>', 'Adjustment value')
1017
+ .option('--description <desc>', 'Description')
1018
+ .option('--fiscal-year <fy>', 'Fiscal year', '2025')
1019
+ .option('--user-id <uuid>', 'User UUID')
1020
+ .action(async (opts) => {
1021
+ try {
1022
+ const path = await saveScenario({
1023
+ name: opts.name,
1024
+ adjustmentType: opts.adjustmentType,
1025
+ adjustmentValue: parseFloat(opts.adjustmentValue),
1026
+ fiscalYear: parseInt(opts.fiscalYear),
1027
+ userId: opts.userId,
1028
+ description: opts.description,
1029
+ });
1030
+ console.log(`Scenario saved: ${path}`);
1031
+ }
1032
+ catch (err) {
1033
+ console.error(`Save failed: ${err instanceof Error ? err.message : String(err)}`);
1034
+ process.exit(1);
1035
+ }
1036
+ });
1037
+ scenario
1038
+ .command('list')
1039
+ .description('List all saved budget scenarios')
1040
+ .action(async () => {
1041
+ const scenarios = await listScenarios();
1042
+ if (scenarios.length === 0) {
1043
+ console.log('No scenarios saved');
1044
+ return;
1045
+ }
1046
+ console.log('| Name | Adjustment | Projected | Change | Created |');
1047
+ console.log('|------|------------|-----------|--------|---------|');
1048
+ for (const s of scenarios) {
1049
+ const adj = s.adjustmentType === 'percentage' ? `${s.adjustmentValue}%` : `+${s.adjustmentValue}`;
1050
+ console.log(`| ${s.name} | ${adj} | ${s.totalProjected.toLocaleString()} | ${s.percentageChange.toFixed(1)}% | ${s.createdAt.slice(0, 10)} |`);
1051
+ }
1052
+ });
1053
+ scenario
1054
+ .command('compare')
1055
+ .description('Compare two or more scenarios side by side')
1056
+ .requiredOption('--names <csv>', 'Comma-separated scenario names')
1057
+ .action(async (opts) => {
1058
+ const names = opts.names.split(',').map(n => n.trim());
1059
+ if (names.length < 2) {
1060
+ console.error('Need at least 2 scenario names to compare');
1061
+ process.exit(1);
1062
+ }
1063
+ try {
1064
+ const result = await compareScenarios(names);
1065
+ // Print header
1066
+ const header = ['Program', 'Actual', ...result.scenarioNames].join(' | ');
1067
+ console.log(`| ${header} |`);
1068
+ console.log(`|${result.scenarioNames.map(() => '---').concat(['---', '---']).join('|')}|`);
1069
+ for (const p of result.programs.slice(0, 50)) {
1070
+ const vals = result.scenarioNames.map(n => String(p.projectedByScenario[n] ?? 0));
1071
+ console.log(`| ${p.programCode} | ${p.actual} | ${vals.join(' | ')} |`);
1072
+ }
1073
+ // Totals
1074
+ console.log('\nTotals:');
1075
+ for (const name of result.scenarioNames) {
1076
+ const t = result.totalsByScenario[name];
1077
+ console.log(` ${name}: ${t.totalProjected.toLocaleString()} (${t.percentageChange >= 0 ? '+' : ''}${t.percentageChange.toFixed(1)}%)`);
1078
+ }
1079
+ }
1080
+ catch (err) {
1081
+ console.error(`Compare failed: ${err instanceof Error ? err.message : String(err)}`);
1082
+ process.exit(1);
1083
+ }
1084
+ });
1085
+ scenario
1086
+ .command('delete')
1087
+ .description('Delete a saved scenario')
1088
+ .requiredOption('--name <name>', 'Scenario name')
1089
+ .action(async (opts) => {
1090
+ try {
1091
+ await deleteScenario(opts.name);
1092
+ console.log(`Deleted scenario: ${opts.name}`);
1093
+ }
1094
+ catch (err) {
1095
+ console.error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
1096
+ process.exit(1);
1097
+ }
1098
+ });
1099
+ // ── Delete batch ────────────────────────────────────────────────────
1100
+ program
1101
+ .command('delete-batch')
1102
+ .description('Batch delete transactions or staging rows (safe: dry-run by default)')
1103
+ .requiredOption('--table <t>', 'Table: transactions or stg_financials_raw')
1104
+ .option('--user-id <uuid>', 'User UUID filter')
1105
+ .option('--date-from <date>', 'Start date YYYY-MM-DD')
1106
+ .option('--date-to <date>', 'End date YYYY-MM-DD')
1107
+ .option('--source <src>', 'Source filter')
1108
+ .option('--category <cat>', 'Category filter (transactions)')
1109
+ .option('--account-code <code>', 'Account code filter (staging)')
1110
+ .option('--month <YYYY-MM>', 'Month filter (staging)')
1111
+ .option('--execute', 'Actually delete (default is dry-run preview)', false)
1112
+ .action(async (opts) => {
1113
+ const table = opts.table;
1114
+ const filters = {
1115
+ dateFrom: opts.dateFrom,
1116
+ dateTo: opts.dateTo,
1117
+ source: opts.source,
1118
+ category: opts.category,
1119
+ accountCode: opts.accountCode,
1120
+ month: opts.month,
1121
+ };
1122
+ const dryRun = !opts.execute;
1123
+ if (dryRun) {
1124
+ const preview = await previewBatch({ table, userId: opts.userId, filters });
1125
+ console.log(`Preview: ${preview.matchCount} rows would be deleted from ${table}`);
1126
+ if (Object.keys(preview.groupedCounts).length > 0) {
1127
+ console.log('\nGrouped:');
1128
+ for (const [key, count] of Object.entries(preview.groupedCounts)) {
1129
+ console.log(` ${key}: ${count}`);
1130
+ }
1131
+ }
1132
+ if (preview.sample.length > 0) {
1133
+ console.log(`\nSample (first ${preview.sample.length}):`);
1134
+ for (const row of preview.sample) {
1135
+ console.log(` ${JSON.stringify(row)}`);
1136
+ }
1137
+ }
1138
+ console.log('\nUse --execute to actually delete');
1139
+ }
1140
+ else {
1141
+ const result = await deleteBatch({ table, userId: opts.userId, filters, dryRun: false });
1142
+ console.log(`Deleted ${result.deletedCount} rows from ${table}`);
1143
+ }
1144
+ });
1145
+ // ── Config registry (v1 scaffold) ─────────────────────────────────
1146
+ const config = program.command('config').description('Manage optimal-cli local/shared config profile')
1147
+ .addHelpText('after', `
1148
+ Examples:
1149
+ $ optimal config init --owner oracle --brand CRE-11TRUST
1150
+ $ optimal config doctor
1151
+ $ optimal config export --out ./backup.json
1152
+ $ optimal config import --in ./backup.json
1153
+ $ optimal config sync pull
1154
+ $ optimal config sync push --agent bot1
1155
+ `);
1156
+ config
1157
+ .command('init')
1158
+ .description('Create a local config scaffold (overwrites with --force)')
1159
+ .option('--owner <owner>', 'Config owner (default: $OPTIMAL_CONFIG_OWNER or $USER)')
1160
+ .option('--profile <name>', 'Profile name', 'default')
1161
+ .option('--brand <brand>', 'Default brand', 'CRE-11TRUST')
1162
+ .option('--timezone <tz>', 'Default timezone', 'America/New_York')
1163
+ .option('--force', 'Overwrite existing config', false)
1164
+ .action(async (opts) => {
1165
+ try {
1166
+ const existing = await readLocalConfig();
1167
+ if (existing && !opts.force) {
1168
+ console.error(`Config already exists at ${getLocalConfigPath()} (use --force to overwrite)`);
1169
+ process.exit(1);
1170
+ }
1171
+ const owner = opts.owner || process.env.OPTIMAL_CONFIG_OWNER || process.env.USER || 'oracle';
1172
+ const payload = {
1173
+ version: '1.0.0',
1174
+ profile: {
1175
+ name: opts.profile,
1176
+ owner,
1177
+ updated_at: new Date().toISOString(),
1178
+ },
1179
+ providers: {
1180
+ supabase: {
1181
+ project_ref: process.env.OPTIMAL_SUPABASE_PROJECT_REF || 'unset',
1182
+ url: process.env.OPTIMAL_SUPABASE_URL || 'unset',
1183
+ anon_key_present: Boolean(process.env.OPTIMAL_SUPABASE_ANON_KEY),
1184
+ },
1185
+ strapi: {
1186
+ base_url: process.env.STRAPI_BASE_URL || 'unset',
1187
+ token_present: Boolean(process.env.STRAPI_TOKEN),
1188
+ },
1189
+ },
1190
+ defaults: {
1191
+ brand: opts.brand,
1192
+ timezone: opts.timezone,
1193
+ },
1194
+ features: {
1195
+ cms: true,
1196
+ tasks: true,
1197
+ deploy: true,
1198
+ },
1199
+ };
1200
+ await writeLocalConfig(payload);
1201
+ await appendHistory(`${new Date().toISOString()} init profile=${opts.profile} owner=${owner} hash=${hashConfig(payload)}`);
1202
+ console.log(`Initialized config at ${getLocalConfigPath()}`);
1203
+ }
1204
+ catch (err) {
1205
+ console.error(`Config init failed: ${err instanceof Error ? err.message : String(err)}`);
1206
+ process.exit(1);
1207
+ }
1208
+ });
1209
+ config
1210
+ .command('doctor')
1211
+ .description('Validate local config file and print health details')
1212
+ .action(async () => {
1213
+ try {
1214
+ const cfg = await readLocalConfig();
1215
+ if (!cfg) {
1216
+ console.log(`No local config found at ${getLocalConfigPath()}`);
1217
+ process.exit(1);
1218
+ }
1219
+ const digest = hashConfig(cfg);
1220
+ console.log(`config: ok`);
1221
+ console.log(`path: ${getLocalConfigPath()}`);
1222
+ console.log(`profile: ${cfg.profile.name}`);
1223
+ console.log(`owner: ${cfg.profile.owner}`);
1224
+ console.log(`version: ${cfg.version}`);
1225
+ console.log(`hash: ${digest}`);
1226
+ console.log(`history: ${getHistoryPath()}`);
1227
+ }
1228
+ catch (err) {
1229
+ console.error(`Config doctor failed: ${err instanceof Error ? err.message : String(err)}`);
1230
+ process.exit(1);
1231
+ }
1232
+ });
1233
+ config
1234
+ .command('export')
1235
+ .description('Export local config to a JSON path')
1236
+ .requiredOption('--out <path>', 'Output path for JSON export')
1237
+ .action(async (opts) => {
1238
+ try {
1239
+ const cfg = await readLocalConfig();
1240
+ if (!cfg) {
1241
+ console.error(`No local config found at ${getLocalConfigPath()}`);
1242
+ process.exit(1);
1243
+ }
1244
+ const payload = {
1245
+ ...cfg,
1246
+ profile: {
1247
+ ...cfg.profile,
1248
+ updated_at: new Date().toISOString(),
1249
+ },
1250
+ };
1251
+ const json = `${JSON.stringify(payload, null, 2)}\n`;
1252
+ writeFileSync(opts.out, json, 'utf-8');
1253
+ await appendHistory(`${new Date().toISOString()} export out=${opts.out} hash=${hashConfig(payload)}`);
1254
+ console.log(`Exported config to ${opts.out}`);
1255
+ }
1256
+ catch (err) {
1257
+ console.error(`Config export failed: ${err instanceof Error ? err.message : String(err)}`);
1258
+ process.exit(1);
1259
+ }
1260
+ });
1261
+ config
1262
+ .command('import')
1263
+ .description('Import local config from a JSON path')
1264
+ .requiredOption('--in <path>', 'Input path for JSON config')
1265
+ .action(async (opts) => {
1266
+ try {
1267
+ if (!existsSync(opts.in)) {
1268
+ console.error(`Input file not found: ${opts.in}`);
1269
+ process.exit(1);
1270
+ }
1271
+ const raw = readFileSync(opts.in, 'utf-8');
1272
+ const parsed = JSON.parse(raw);
1273
+ const payload = assertOptimalConfigV1(parsed);
1274
+ await writeLocalConfig(payload);
1275
+ await appendHistory(`${new Date().toISOString()} import in=${opts.in} hash=${hashConfig(payload)}`);
1276
+ console.log(`Imported config from ${opts.in}`);
1277
+ }
1278
+ catch (err) {
1279
+ console.error(`Config import failed: ${err instanceof Error ? err.message : String(err)}`);
1280
+ process.exit(1);
1281
+ }
1282
+ });
1283
+ const configSync = config.command('sync').description('Sync local profile with shared registry (scaffold)');
1284
+ configSync
1285
+ .command('pull')
1286
+ .description('Pull config profile from shared registry into local config')
1287
+ .option('--profile <name>', 'Registry profile name', 'default')
1288
+ .action(async (opts) => {
1289
+ const result = await pullRegistryProfile(opts.profile);
1290
+ const stamp = new Date().toISOString();
1291
+ await appendHistory(`${stamp} sync.pull profile=${opts.profile} ok=${result.ok} msg=${result.message}`);
1292
+ if (!result.ok) {
1293
+ console.error(result.message);
1294
+ process.exit(1);
1295
+ }
1296
+ console.log(result.message);
1297
+ });
1298
+ configSync
1299
+ .command('push')
1300
+ .description('Push local config profile to shared registry')
1301
+ .requiredOption('--agent <name>', 'Agent/owner name for the config profile')
1302
+ .option('--profile <name>', 'Registry profile name', 'default')
1303
+ .option('--force', 'Force write even on conflict', false)
1304
+ .action(async (opts) => {
1305
+ const result = await pushRegistryProfile(opts.profile, Boolean(opts.force), opts.agent);
1306
+ const stamp = new Date().toISOString();
1307
+ await appendHistory(`${stamp} sync.push agent=${opts.agent} profile=${opts.profile} force=${Boolean(opts.force)} ok=${result.ok} msg=${result.message}`);
1308
+ if (!result.ok) {
1309
+ console.error(result.message);
1310
+ process.exit(1);
1311
+ }
1312
+ console.log(result.message);
1313
+ });
1314
+ // --- Bot commands ---
1315
+ const bot = program.command('bot').description('Bot agent orchestration')
1316
+ .addHelpText('after', `
1317
+ Examples:
1318
+ $ optimal bot agents List active agents
1319
+ $ optimal bot heartbeat --agent bot1 Send heartbeat
1320
+ $ optimal bot claim --agent bot1 Claim next task
1321
+ $ optimal bot report --task <id> --agent bot1 --message "50% done"
1322
+ $ optimal bot complete --task <id> --agent bot1 --summary "All tests pass"
1323
+ `);
1324
+ bot
1325
+ .command('heartbeat')
1326
+ .description('Send agent heartbeat')
1327
+ .requiredOption('--agent <id>', 'Agent ID')
1328
+ .option('--status <s>', 'Status: idle, working, error', 'idle')
1329
+ .action(async (opts) => {
1330
+ await sendHeartbeat(opts.agent, opts.status);
1331
+ success(`Heartbeat sent: ${colorize(opts.agent, 'bold')} [${colorize(opts.status, 'cyan')}]`);
1332
+ });
1333
+ bot
1334
+ .command('agents')
1335
+ .description('List active agents (heartbeat in last 5 min)')
1336
+ .action(async () => {
1337
+ const agents = await getActiveAgents();
1338
+ if (agents.length === 0) {
1339
+ console.log('No active agents.');
1340
+ return;
1341
+ }
1342
+ console.log('| Agent | Status | Last Seen |');
1343
+ console.log('|------------------|---------|---------------------|');
1344
+ for (const a of agents) {
1345
+ console.log(`| ${a.agent.padEnd(16)} | ${a.status.padEnd(7)} | ${a.lastSeen} |`);
1346
+ }
1347
+ });
1348
+ bot
1349
+ .command('claim')
1350
+ .description('Claim the next available task')
1351
+ .requiredOption('--agent <id>', 'Agent ID')
1352
+ .option('--skill <s>', 'Skill filter (comma-separated)')
1353
+ .action(async (opts) => {
1354
+ const skills = opts.skill ? opts.skill.split(',') : undefined;
1355
+ const task = await claimNextTask(opts.agent, skills);
1356
+ if (!task) {
1357
+ console.log('No claimable tasks found.');
1358
+ return;
1359
+ }
1360
+ success(`Claimed: ${colorize(task.title, 'cyan')} (${colorize(task.id, 'dim')}) by ${colorize(opts.agent, 'bold')}`);
1361
+ });
1362
+ bot
1363
+ .command('report')
1364
+ .description('Report progress on a task')
1365
+ .requiredOption('--task <id>', 'Task ID')
1366
+ .requiredOption('--agent <id>', 'Agent ID')
1367
+ .requiredOption('--message <msg>', 'Progress message')
1368
+ .action(async (opts) => {
1369
+ await reportProgress(opts.task, opts.agent, opts.message);
1370
+ success(`Progress reported on ${colorize(opts.task, 'dim')}`);
1371
+ });
1372
+ bot
1373
+ .command('complete')
1374
+ .description('Mark a task as done')
1375
+ .requiredOption('--task <id>', 'Task ID')
1376
+ .requiredOption('--agent <id>', 'Agent ID')
1377
+ .requiredOption('--summary <s>', 'Completion summary')
1378
+ .action(async (opts) => {
1379
+ await reportCompletion(opts.task, opts.agent, opts.summary);
1380
+ success(`Task ${colorize(opts.task, 'dim')} marked ${statusBadge('done')} by ${colorize(opts.agent, 'bold')}`);
1381
+ });
1382
+ bot
1383
+ .command('release')
1384
+ .description('Release a claimed task back to ready')
1385
+ .requiredOption('--task <id>', 'Task ID')
1386
+ .requiredOption('--agent <id>', 'Agent ID')
1387
+ .option('--reason <r>', 'Release reason')
1388
+ .action(async (opts) => {
1389
+ await releaseTask(opts.task, opts.agent, opts.reason);
1390
+ fmtInfo(`Task ${colorize(opts.task, 'dim')} released by ${colorize(opts.agent, 'bold')}`);
1391
+ });
1392
+ bot
1393
+ .command('blocked')
1394
+ .description('Mark a task as blocked')
1395
+ .requiredOption('--task <id>', 'Task ID')
1396
+ .requiredOption('--agent <id>', 'Agent ID')
1397
+ .requiredOption('--reason <r>', 'Block reason')
1398
+ .action(async (opts) => {
1399
+ await reportBlocked(opts.task, opts.agent, opts.reason);
1400
+ fmtWarn(`Task ${colorize(opts.task, 'dim')} marked ${statusBadge('blocked')}: ${opts.reason}`);
1401
+ });
1402
+ // --- Coordinator commands ---
1403
+ const coordinator = program.command('coordinator').description('Multi-agent coordination')
1404
+ .addHelpText('after', `
1405
+ Examples:
1406
+ $ optimal coordinator start Run coordinator loop
1407
+ $ optimal coordinator start --interval 10000 Poll every 10s
1408
+ $ optimal coordinator status Show coordinator status
1409
+ $ optimal coordinator assign --task <id> --agent bot1
1410
+ $ optimal coordinator rebalance Release stale tasks
1411
+ `);
1412
+ coordinator
1413
+ .command('start')
1414
+ .description('Run the coordinator loop')
1415
+ .option('--interval <ms>', 'Poll interval in milliseconds', '30000')
1416
+ .option('--max-agents <n>', 'Maximum agents to manage', '10')
1417
+ .action(async (opts) => {
1418
+ await runCoordinatorLoop({
1419
+ pollIntervalMs: parseInt(opts.interval),
1420
+ maxAgents: parseInt(opts.maxAgents),
1421
+ });
1422
+ });
1423
+ coordinator
1424
+ .command('status')
1425
+ .description('Show coordinator status')
1426
+ .action(async () => {
1427
+ const s = await getCoordinatorStatus();
1428
+ console.log(`Last poll: ${s.lastPollAt ?? 'never'}`);
1429
+ console.log(`Tasks — ready: ${s.tasksReady}, in progress: ${s.tasksInProgress}, blocked: ${s.tasksBlocked}`);
1430
+ console.log(`\nActive agents (${s.activeAgents.length}):`);
1431
+ for (const a of s.activeAgents) {
1432
+ console.log(` ${a.agent.padEnd(16)} ${a.status.padEnd(8)} last seen ${a.lastSeen}`);
1433
+ }
1434
+ console.log(`\nIdle agents (${s.idleAgents.length}):`);
1435
+ for (const a of s.idleAgents) {
1436
+ console.log(` ${a.id.padEnd(16)} skills: ${a.skills.join(', ')}`);
1437
+ }
1438
+ });
1439
+ coordinator
1440
+ .command('assign')
1441
+ .description('Manually assign a task to an agent')
1442
+ .requiredOption('--task <id>', 'Task ID')
1443
+ .requiredOption('--agent <id>', 'Agent ID')
1444
+ .action(async (opts) => {
1445
+ const task = await assignTask(opts.task, opts.agent);
1446
+ success(`Assigned: ${colorize(task.title, 'cyan')} -> ${colorize(opts.agent, 'bold')}`);
1447
+ });
1448
+ coordinator
1449
+ .command('rebalance')
1450
+ .description('Release stale tasks and rebalance')
1451
+ .action(async () => {
1452
+ const result = await rebalance();
1453
+ if (result.releasedTasks.length === 0) {
1454
+ fmtInfo('No stale tasks found.');
1455
+ return;
1456
+ }
1457
+ console.log(`Released ${result.releasedTasks.length} stale task(s):`);
1458
+ for (const t of result.releasedTasks) {
1459
+ console.log(` ${colorize(t.id, 'dim')} ${t.title}`);
1460
+ }
1461
+ if (result.reassignedTasks.length > 0) {
1462
+ console.log(`Reassigned ${result.reassignedTasks.length} task(s):`);
1463
+ for (const t of result.reassignedTasks) {
1464
+ console.log(` ${colorize(t.id, 'dim')} ${t.title} -> ${t.claimed_by}`);
1465
+ }
1466
+ }
1467
+ });
1468
+ // --- Asset commands ---
1469
+ const asset = program.command('asset').description('Digital asset tracking (domains, servers, API keys, services, repos)')
1470
+ .addHelpText('after', `
1471
+ Examples:
1472
+ $ optimal asset list List all assets
1473
+ $ optimal asset list --type domain --status active Filter by type/status
1474
+ $ optimal asset add --name "op-hub.com" --type domain --owner clenisa
1475
+ $ optimal asset update --id <uuid> --status inactive
1476
+ $ optimal asset usage --id <uuid> View usage log
1477
+ `);
1478
+ asset
1479
+ .command('list')
1480
+ .description('List tracked assets')
1481
+ .option('-t, --type <type>', 'Filter by type (domain, server, api_key, service, repo, other)')
1482
+ .option('-s, --status <status>', 'Filter by status (active, inactive, expired, pending)')
1483
+ .option('-o, --owner <owner>', 'Filter by owner')
1484
+ .option('--json', 'Output as JSON')
1485
+ .action(async (opts) => {
1486
+ const assets = await listAssets({
1487
+ type: opts.type,
1488
+ status: opts.status,
1489
+ owner: opts.owner,
1490
+ });
1491
+ if (opts.json) {
1492
+ console.log(JSON.stringify(assets, null, 2));
1493
+ }
1494
+ else {
1495
+ console.log(formatAssetTable(assets));
1496
+ }
1497
+ });
1498
+ asset
1499
+ .command('add')
1500
+ .description('Add a new asset')
1501
+ .requiredOption('-n, --name <name>', 'Asset name')
1502
+ .requiredOption('-t, --type <type>', 'Asset type (domain, server, api_key, service, repo, other)')
1503
+ .option('-s, --status <status>', 'Status (default: active)')
1504
+ .option('-o, --owner <owner>', 'Owner')
1505
+ .option('--expires <date>', 'Expiration date (YYYY-MM-DD or ISO)')
1506
+ .option('--meta <json>', 'Metadata JSON string')
1507
+ .action(async (opts) => {
1508
+ const metadata = opts.meta ? JSON.parse(opts.meta) : undefined;
1509
+ const created = await createAsset({
1510
+ name: opts.name,
1511
+ type: opts.type,
1512
+ status: opts.status,
1513
+ owner: opts.owner,
1514
+ expires_at: opts.expires,
1515
+ metadata,
1516
+ });
1517
+ success(`Created asset: ${colorize(created.name, 'cyan')} [${created.type}] (${colorize(created.id, 'dim')})`);
1518
+ });
1519
+ asset
1520
+ .command('update')
1521
+ .description('Update an existing asset')
1522
+ .requiredOption('--id <uuid>', 'Asset ID')
1523
+ .option('-n, --name <name>', 'New name')
1524
+ .option('-t, --type <type>', 'New type')
1525
+ .option('-s, --status <status>', 'New status')
1526
+ .option('-o, --owner <owner>', 'New owner')
1527
+ .option('--expires <date>', 'New expiration date')
1528
+ .option('--meta <json>', 'New metadata JSON')
1529
+ .action(async (opts) => {
1530
+ const updates = {};
1531
+ if (opts.name)
1532
+ updates.name = opts.name;
1533
+ if (opts.type)
1534
+ updates.type = opts.type;
1535
+ if (opts.status)
1536
+ updates.status = opts.status;
1537
+ if (opts.owner)
1538
+ updates.owner = opts.owner;
1539
+ if (opts.expires)
1540
+ updates.expires_at = opts.expires;
1541
+ if (opts.meta)
1542
+ updates.metadata = JSON.parse(opts.meta);
1543
+ const updated = await updateAsset(opts.id, updates);
1544
+ success(`Updated: ${colorize(updated.name, 'cyan')} -> status=${colorize(updated.status, 'bold')}`);
1545
+ });
1546
+ asset
1547
+ .command('get')
1548
+ .description('Get a single asset by ID')
1549
+ .requiredOption('--id <uuid>', 'Asset ID')
1550
+ .action(async (opts) => {
1551
+ const a = await getAsset(opts.id);
1552
+ console.log(JSON.stringify(a, null, 2));
1553
+ });
1554
+ asset
1555
+ .command('remove')
1556
+ .description('Delete an asset')
1557
+ .requiredOption('--id <uuid>', 'Asset ID')
1558
+ .action(async (opts) => {
1559
+ await deleteAsset(opts.id);
1560
+ success(`Deleted asset ${colorize(opts.id, 'dim')}`);
1561
+ });
1562
+ asset
1563
+ .command('track')
1564
+ .description('Log a usage event for an asset')
1565
+ .requiredOption('--id <uuid>', 'Asset ID')
1566
+ .requiredOption('-e, --event <event>', 'Event name (e.g. "renewed", "deployed", "rotated")')
1567
+ .option('--actor <name>', 'Who performed the action')
1568
+ .option('--meta <json>', 'Event metadata JSON')
1569
+ .action(async (opts) => {
1570
+ const metadata = opts.meta ? JSON.parse(opts.meta) : undefined;
1571
+ const entry = await trackAssetUsage(opts.id, opts.event, opts.actor, metadata);
1572
+ success(`Tracked: ${colorize(opts.event, 'cyan')} on ${colorize(opts.id, 'dim')} at ${colorize(entry.created_at, 'dim')}`);
1573
+ });
1574
+ asset
1575
+ .command('usage')
1576
+ .description('View usage log for an asset')
1577
+ .requiredOption('--id <uuid>', 'Asset ID')
1578
+ .option('--limit <n>', 'Max entries', '20')
1579
+ .action(async (opts) => {
1580
+ const events = await listAssetUsage(opts.id, parseInt(opts.limit));
1581
+ if (events.length === 0) {
1582
+ console.log('No usage events found.');
1583
+ return;
1584
+ }
1585
+ for (const e of events) {
1586
+ console.log(`${e.created_at} | ${(e.actor ?? '-').padEnd(10)} | ${e.event} ${Object.keys(e.metadata).length > 0 ? JSON.stringify(e.metadata) : ''}`);
1587
+ }
1588
+ console.log(`\n${events.length} events`);
1589
+ });
1590
+ program.parseAsync();