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
@@ -1,12 +1,12 @@
1
- #!/usr/bin/env tsx
1
+ #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import 'dotenv/config';
4
- import { getBoard, createTask, updateTask, logActivity, } from '../lib/kanban.js';
4
+ import { createProject, getProjectBySlug, listProjects, updateProject, createMilestone, listMilestones, createLabel, listLabels, createTask, updateTask, listTasks, claimTask, addComment, listActivity, formatBoardTable, } from '../lib/board/index.js';
5
5
  import { runAuditComparison } from '../lib/returnpro/audit.js';
6
6
  import { exportKpis, formatKpiTable, formatKpiCsv } from '../lib/returnpro/kpis.js';
7
7
  import { deploy, healthCheck, listApps } from '../lib/infra/deploy.js';
8
8
  import { fetchWesImports, parseSummaryFromJson, initializeProjections, applyUniformAdjustment, calculateTotals, exportToCSV, formatProjectionTable, } from '../lib/budget/projections.js';
9
- import { readFileSync, existsSync } from 'node:fs';
9
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
10
10
  import { generateNewsletter } from '../lib/newsletter/generate.js';
11
11
  import { scrapeCompanies, formatCsv } from '../lib/social/scraper.js';
12
12
  import { ingestTransactions } from '../lib/transactions/ingest.js';
@@ -21,87 +21,258 @@ import { distributeNewsletter, checkDistributionStatus } from '../lib/newsletter
21
21
  import { generateSocialPosts } from '../lib/social/post-generator.js';
22
22
  import { publishSocialPosts, getPublishQueue, retryFailed } from '../lib/social/publish.js';
23
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';
24
26
  import { migrateDb, listPendingMigrations, createMigration } from '../lib/infra/migrate.js';
25
27
  import { saveScenario, listScenarios, compareScenarios, deleteScenario } from '../lib/budget/scenarios.js';
26
28
  import { deleteBatch, previewBatch } from '../lib/transactions/delete-batch.js';
27
- import { pushConfig, pullConfig, listConfigs, diffConfig, syncConfig } from '../lib/config.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';
28
34
  const program = new Command()
29
35
  .name('optimal')
30
36
  .description('Optimal CLI — unified skills for financial analytics, content, and infra')
31
- .version('0.1.0');
32
- // Board commands
33
- const board = program.command('board').description('Kanban board operations');
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
+ `);
34
64
  board
35
65
  .command('view')
36
66
  .description('Display the kanban board')
37
- .option('-p, --project <slug>', 'Project slug', 'optimal-cli-refactor')
67
+ .option('-p, --project <slug>', 'Project slug')
38
68
  .option('-s, --status <status>', 'Filter by status')
69
+ .option('--mine <agent>', 'Show only tasks claimed by agent')
39
70
  .action(async (opts) => {
40
- let tasks = await getBoard(opts.project);
41
- if (opts.status)
42
- tasks = tasks.filter(t => t.status === opts.status);
43
- const grouped = new Map();
44
- for (const t of tasks) {
45
- const list = grouped.get(t.status) ?? [];
46
- list.push(t);
47
- grouped.set(t.status, list);
48
- }
49
- const order = ['in_progress', 'blocked', 'ready', 'backlog', 'review', 'done'];
50
- console.log('| Status | P | Title | Agent | Skill |');
51
- console.log('|--------|---|-------|-------|-------|');
52
- for (const status of order) {
53
- const list = grouped.get(status) ?? [];
54
- for (const t of list) {
55
- console.log(`| ${t.status} | ${t.priority} | ${t.title} | ${t.assigned_agent ?? '—'} | ${t.skill_ref ?? '—'} |`);
56
- }
71
+ const filters = {};
72
+ if (opts.project) {
73
+ const proj = await getProjectBySlug(opts.project);
74
+ filters.project_id = proj.id;
57
75
  }
58
- console.log(`\nTotal: ${tasks.length} tasks`);
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));
59
82
  });
60
83
  board
61
84
  .command('create')
62
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
+ `)
63
90
  .requiredOption('-t, --title <title>', 'Task title')
64
- .option('-p, --project <slug>', 'Project slug', 'optimal-cli-refactor')
91
+ .requiredOption('-p, --project <slug>', 'Project slug')
65
92
  .option('-d, --description <desc>', 'Task description')
66
93
  .option('--priority <n>', 'Priority 1-4', '3')
67
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')
68
99
  .option('--labels <labels>', 'Comma-separated labels')
69
100
  .action(async (opts) => {
101
+ const project = await getProjectBySlug(opts.project);
70
102
  const task = await createTask({
71
- project_slug: opts.project,
103
+ project_id: project.id,
72
104
  title: opts.title,
73
105
  description: opts.description,
74
106
  priority: parseInt(opts.priority),
75
- skill_ref: opts.skill,
76
- labels: opts.labels?.split(',').map((l) => l.trim()),
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(',') ?? [],
77
113
  });
78
- console.log(`Created task: ${task.id} — "${task.title}" (priority ${task.priority}, status ${task.status})`);
114
+ success(`Created task: ${colorize(task.id, 'dim')}\n ${task.title} [${statusBadge(task.status)}] ${priorityBadge(task.priority)}`);
79
115
  });
80
116
  board
81
117
  .command('update')
82
118
  .description('Update a task')
83
- .requiredOption('--id <taskId>', 'Task UUID')
119
+ .requiredOption('--id <uuid>', 'Task ID')
84
120
  .option('-s, --status <status>', 'New status')
85
121
  .option('-a, --agent <name>', 'Assign to agent')
86
122
  .option('--priority <n>', 'New priority')
87
- .option('-m, --message <msg>', 'Log message')
123
+ .option('-m, --message <msg>', 'Log message (adds comment)')
88
124
  .action(async (opts) => {
89
125
  const updates = {};
90
126
  if (opts.status)
91
127
  updates.status = opts.status;
92
128
  if (opts.agent)
93
- updates.assigned_agent = opts.agent;
129
+ updates.assigned_to = opts.agent;
94
130
  if (opts.priority)
95
131
  updates.priority = parseInt(opts.priority);
96
- const task = await updateTask(opts.id, updates);
97
- if (opts.message) {
98
- await logActivity(opts.id, {
99
- agent: opts.agent ?? 'cli',
100
- action: 'status_change',
101
- message: opts.message,
102
- });
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}`);
103
256
  }
104
- console.log(`Updated task ${task.id}: status → ${task.status}, agent → ${task.assigned_agent ?? '—'}`);
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})` : ''}`);
105
276
  });
106
277
  // Audit financials command
107
278
  program
@@ -186,14 +357,14 @@ program
186
357
  .argument('<app>', `App to deploy (${listApps().join(', ')})`)
187
358
  .option('--prod', 'Deploy to production', false)
188
359
  .action(async (app, opts) => {
189
- console.log(`Deploying ${app}${opts.prod ? ' (production)' : ' (preview)'}...`);
360
+ fmtInfo(`Deploying ${colorize(app, 'cyan')}${opts.prod ? colorize(' (production)', 'yellow') : ' (preview)'}...`);
190
361
  try {
191
362
  const url = await deploy(app, opts.prod);
192
- console.log(`Deployed: ${url}`);
363
+ success(`Deployed: ${colorize(url, 'green')}`);
193
364
  }
194
365
  catch (err) {
195
366
  const msg = err instanceof Error ? err.message : String(err);
196
- console.error(`Deploy failed: ${msg}`);
367
+ fmtError(`Deploy failed: ${msg}`);
197
368
  process.exit(1);
198
369
  }
199
370
  });
@@ -298,12 +469,12 @@ program
298
469
  dryRun: opts.dryRun,
299
470
  });
300
471
  if (result.strapiDocumentId) {
301
- console.log(`\nStrapi documentId: ${result.strapiDocumentId}`);
472
+ success(`Strapi documentId: ${colorize(result.strapiDocumentId, 'cyan')}`);
302
473
  }
303
474
  }
304
475
  catch (err) {
305
476
  const msg = err instanceof Error ? err.message : String(err);
306
- console.error(`Newsletter generation failed: ${msg}`);
477
+ fmtError(`Newsletter generation failed: ${msg}`);
307
478
  process.exit(1);
308
479
  }
309
480
  });
@@ -676,6 +847,71 @@ program
676
847
  }
677
848
  console.log(`\n${queue.length} posts queued`);
678
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
+ });
679
915
  // ── Publish blog ────────────────────────────────────────────────────
680
916
  program
681
917
  .command('publish-blog')
@@ -712,7 +948,13 @@ program
712
948
  }
713
949
  });
714
950
  // ── Database migration ──────────────────────────────────────────────
715
- const migrate = program.command('migrate').description('Supabase database migration operations');
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
+ `);
716
958
  migrate
717
959
  .command('push')
718
960
  .description('Run supabase db push --linked on a target project')
@@ -758,7 +1000,14 @@ migrate
758
1000
  console.log(`Created: ${path}`);
759
1001
  });
760
1002
  // ── Budget scenarios ────────────────────────────────────────────────
761
- const scenario = program.command('scenario').description('Budget scenario management');
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
+ `);
762
1011
  scenario
763
1012
  .command('save')
764
1013
  .description('Save current projections as a named scenario')
@@ -893,103 +1142,449 @@ program
893
1142
  console.log(`Deleted ${result.deletedCount} rows from ${table}`);
894
1143
  }
895
1144
  });
896
- // ── Config commands ─────────────────────────────────────────────────
897
- const config = program.command('config').description('OpenClaw config sync operations');
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
+ `);
898
1156
  config
899
- .command('push')
900
- .description('Push local openclaw.json to cloud storage')
901
- .requiredOption('--agent <name>', 'Agent name (e.g., oracle, opal, kimklaw)')
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)
902
1164
  .action(async (opts) => {
903
1165
  try {
904
- const result = await pushConfig(opts.agent);
905
- console.log(`✓ Config pushed for ${opts.agent}`);
906
- console.log(` ID: ${result.id}`);
907
- console.log(` Version: ${result.version}`);
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()}`);
908
1203
  }
909
1204
  catch (err) {
910
- console.error(`Push failed: ${err instanceof Error ? err.message : String(err)}`);
1205
+ console.error(`Config init failed: ${err instanceof Error ? err.message : String(err)}`);
911
1206
  process.exit(1);
912
1207
  }
913
1208
  });
914
1209
  config
915
- .command('pull')
916
- .description('Pull config from cloud and save to local openclaw.json')
917
- .requiredOption('--agent <name>', 'Agent name to pull config for')
918
- .action(async (opts) => {
1210
+ .command('doctor')
1211
+ .description('Validate local config file and print health details')
1212
+ .action(async () => {
919
1213
  try {
920
- const result = await pullConfig(opts.agent);
921
- console.log(`✓ Config pulled for ${result.agent_name}`);
922
- console.log(` Version: ${result.version}`);
923
- console.log(` Updated: ${result.updated_at}`);
924
- console.log(`\nSaved to: ~/.openclaw/openclaw.json`);
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()}`);
925
1227
  }
926
1228
  catch (err) {
927
- console.error(`Pull failed: ${err instanceof Error ? err.message : String(err)}`);
1229
+ console.error(`Config doctor failed: ${err instanceof Error ? err.message : String(err)}`);
928
1230
  process.exit(1);
929
1231
  }
930
1232
  });
931
1233
  config
932
- .command('list')
933
- .description('List all saved agent configs in cloud')
934
- .action(async () => {
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) => {
935
1238
  try {
936
- const configs = await listConfigs();
937
- if (configs.length === 0) {
938
- console.log('No configs found in cloud storage');
939
- return;
940
- }
941
- console.log('| Agent | Version | Updated |');
942
- console.log('|-------|---------|---------|');
943
- for (const c of configs) {
944
- console.log(`| ${c.agent_name} | ${c.version.slice(0, 19)} | ${c.updated_at.slice(0, 19)} |`);
1239
+ const cfg = await readLocalConfig();
1240
+ if (!cfg) {
1241
+ console.error(`No local config found at ${getLocalConfigPath()}`);
1242
+ process.exit(1);
945
1243
  }
946
- console.log(`\nTotal: ${configs.length} configs`);
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}`);
947
1255
  }
948
1256
  catch (err) {
949
- console.error(`List failed: ${err instanceof Error ? err.message : String(err)}`);
1257
+ console.error(`Config export failed: ${err instanceof Error ? err.message : String(err)}`);
950
1258
  process.exit(1);
951
1259
  }
952
1260
  });
953
1261
  config
954
- .command('diff')
955
- .description('Compare local config with cloud version')
956
- .requiredOption('--agent <name>', 'Agent name to compare')
1262
+ .command('import')
1263
+ .description('Import local config from a JSON path')
1264
+ .requiredOption('--in <path>', 'Input path for JSON config')
957
1265
  .action(async (opts) => {
958
1266
  try {
959
- const { local, cloud, differences } = await diffConfig(opts.agent);
960
- if (differences.length === 0) {
961
- console.log('✓ Local and cloud configs are in sync');
962
- return;
963
- }
964
- console.log('Differences found:');
965
- for (const d of differences) {
966
- console.log(` • ${d}`);
967
- }
968
- if (local?.meta?.lastTouchedAt) {
969
- console.log(`\nLocal updated: ${local.meta.lastTouchedAt}`);
970
- }
971
- if (cloud?.updated_at) {
972
- console.log(`Cloud updated: ${cloud.updated_at}`);
1267
+ if (!existsSync(opts.in)) {
1268
+ console.error(`Input file not found: ${opts.in}`);
1269
+ process.exit(1);
973
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}`);
974
1277
  }
975
1278
  catch (err) {
976
- console.error(`Diff failed: ${err instanceof Error ? err.message : String(err)}`);
1279
+ console.error(`Config import failed: ${err instanceof Error ? err.message : String(err)}`);
977
1280
  process.exit(1);
978
1281
  }
979
1282
  });
980
- config
981
- .command('sync')
982
- .description('Two-way sync: push if local is newer, pull if cloud is newer')
983
- .requiredOption('--agent <name>', 'Agent name to sync')
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')
984
1288
  .action(async (opts) => {
985
- try {
986
- const result = await syncConfig(opts.agent);
987
- console.log(`✓ ${result.action}`);
988
- console.log(` ${result.message}`);
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);
989
1295
  }
990
- catch (err) {
991
- console.error(`Sync failed: ${err instanceof Error ? err.message : String(err)}`);
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);
992
1310
  process.exit(1);
993
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`);
994
1589
  });
995
1590
  program.parseAsync();