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.
- package/agents/.gitkeep +0 -0
- package/agents/content-ops.md +227 -0
- package/agents/financial-ops.md +184 -0
- package/agents/infra-ops.md +206 -0
- package/agents/profiles.json +5 -0
- package/dist/bin/optimal.d.ts +1 -1
- package/dist/bin/optimal.js +706 -111
- package/dist/lib/assets/index.d.ts +79 -0
- package/dist/lib/assets/index.js +153 -0
- package/dist/lib/assets.d.ts +20 -0
- package/dist/lib/assets.js +112 -0
- package/dist/lib/auth/index.d.ts +83 -0
- package/dist/lib/auth/index.js +146 -0
- package/dist/lib/board/index.d.ts +39 -0
- package/dist/lib/board/index.js +285 -0
- package/dist/lib/board/types.d.ts +111 -0
- package/dist/lib/board/types.js +1 -0
- package/dist/lib/bot/claim.d.ts +3 -0
- package/dist/lib/bot/claim.js +20 -0
- package/dist/lib/bot/coordinator.d.ts +27 -0
- package/dist/lib/bot/coordinator.js +178 -0
- package/dist/lib/bot/heartbeat.d.ts +6 -0
- package/dist/lib/bot/heartbeat.js +30 -0
- package/dist/lib/bot/index.d.ts +9 -0
- package/dist/lib/bot/index.js +6 -0
- package/dist/lib/bot/protocol.d.ts +12 -0
- package/dist/lib/bot/protocol.js +74 -0
- package/dist/lib/bot/reporter.d.ts +3 -0
- package/dist/lib/bot/reporter.js +27 -0
- package/dist/lib/bot/skills.d.ts +26 -0
- package/dist/lib/bot/skills.js +69 -0
- package/dist/lib/config/registry.d.ts +17 -0
- package/dist/lib/config/registry.js +182 -0
- package/dist/lib/config/schema.d.ts +31 -0
- package/dist/lib/config/schema.js +25 -0
- package/dist/lib/errors.d.ts +25 -0
- package/dist/lib/errors.js +91 -0
- package/dist/lib/format.d.ts +28 -0
- package/dist/lib/format.js +98 -0
- package/dist/lib/returnpro/validate.d.ts +37 -0
- package/dist/lib/returnpro/validate.js +124 -0
- package/dist/lib/social/meta.d.ts +90 -0
- package/dist/lib/social/meta.js +160 -0
- package/docs/CLI-REFERENCE.md +361 -0
- package/package.json +13 -24
- package/dist/lib/kanban.d.ts +0 -46
- package/dist/lib/kanban.js +0 -118
package/dist/bin/optimal.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import 'dotenv/config';
|
|
4
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
33
|
-
|
|
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'
|
|
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
|
-
|
|
41
|
-
if (opts.
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
103
|
+
project_id: project.id,
|
|
72
104
|
title: opts.title,
|
|
73
105
|
description: opts.description,
|
|
74
106
|
priority: parseInt(opts.priority),
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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 <
|
|
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.
|
|
129
|
+
updates.assigned_to = opts.agent;
|
|
94
130
|
if (opts.priority)
|
|
95
131
|
updates.priority = parseInt(opts.priority);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
+
success(`Deployed: ${colorize(url, 'green')}`);
|
|
193
364
|
}
|
|
194
365
|
catch (err) {
|
|
195
366
|
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
897
|
-
const config = program.command('config').description('
|
|
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('
|
|
900
|
-
.description('
|
|
901
|
-
.
|
|
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
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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(`
|
|
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('
|
|
916
|
-
.description('
|
|
917
|
-
.
|
|
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
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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(`
|
|
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('
|
|
933
|
-
.description('
|
|
934
|
-
.
|
|
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
|
|
937
|
-
if (
|
|
938
|
-
console.
|
|
939
|
-
|
|
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
|
-
|
|
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(`
|
|
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('
|
|
955
|
-
.description('
|
|
956
|
-
.requiredOption('--
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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(`
|
|
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
|
-
|
|
982
|
-
.
|
|
983
|
-
.
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
991
|
-
|
|
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();
|