optimal-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.env.example +17 -0
- package/CLAUDE.md +67 -0
- package/COMMANDS.md +264 -0
- package/PUBLISH.md +70 -0
- package/agents/content-ops.md +2 -2
- package/agents/financial-ops.md +2 -2
- package/agents/infra-ops.md +2 -2
- package/apps/.gitkeep +0 -0
- package/bin/optimal.ts +278 -591
- package/docs/MIGRATION_NEEDED.md +37 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/optimal-cli-config-registry-v1.md +71 -0
- package/hooks/.gitkeep +0 -0
- package/lib/config/registry.ts +5 -4
- package/lib/kanban-obsidian.ts +232 -0
- package/lib/kanban-sync.ts +258 -0
- package/lib/kanban.ts +239 -0
- package/lib/obsidian-tasks.ts +231 -0
- package/package.json +5 -19
- package/pnpm-workspace.yaml +3 -0
- package/scripts/check-table.ts +24 -0
- package/scripts/create-tables.ts +94 -0
- package/scripts/migrate-kanban.sh +28 -0
- package/scripts/migrate-v2.ts +78 -0
- package/scripts/migrate.ts +79 -0
- package/scripts/run-migration.ts +59 -0
- package/scripts/seed-board.ts +203 -0
- package/scripts/test-kanban.ts +21 -0
- package/skills/audit-financials/SKILL.md +33 -0
- package/skills/board-create/SKILL.md +28 -0
- package/skills/board-update/SKILL.md +27 -0
- package/skills/board-view/SKILL.md +27 -0
- package/skills/delete-batch/SKILL.md +77 -0
- package/skills/deploy/SKILL.md +40 -0
- package/skills/diagnose-months/SKILL.md +68 -0
- package/skills/distribute-newsletter/SKILL.md +58 -0
- package/skills/export-budget/SKILL.md +44 -0
- package/skills/export-kpis/SKILL.md +52 -0
- package/skills/generate-netsuite-template/SKILL.md +51 -0
- package/skills/generate-newsletter/SKILL.md +53 -0
- package/skills/generate-newsletter-insurance/SKILL.md +59 -0
- package/skills/generate-social-posts/SKILL.md +67 -0
- package/skills/health-check/SKILL.md +42 -0
- package/skills/ingest-transactions/SKILL.md +51 -0
- package/skills/manage-cms/SKILL.md +50 -0
- package/skills/manage-scenarios/SKILL.md +83 -0
- package/skills/migrate-db/SKILL.md +79 -0
- package/skills/preview-newsletter/SKILL.md +50 -0
- package/skills/project-budget/SKILL.md +60 -0
- package/skills/publish-blog/SKILL.md +70 -0
- package/skills/publish-social-posts/SKILL.md +70 -0
- package/skills/rate-anomalies/SKILL.md +62 -0
- package/skills/scrape-ads/SKILL.md +49 -0
- package/skills/stamp-transactions/SKILL.md +62 -0
- package/skills/upload-income-statements/SKILL.md +54 -0
- package/skills/upload-netsuite/SKILL.md +56 -0
- package/skills/upload-r1/SKILL.md +45 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/.gitkeep +0 -0
- package/supabase/migrations/20250305000001_create_agent_configs.sql +36 -0
- package/supabase/migrations/20260305111300_create_cli_config_registry.sql +22 -0
- package/supabase/migrations/20260306195000_create_kanban_tables.sql +97 -0
- package/tests/config-command-smoke.test.ts +395 -0
- package/tests/config-registry.test.ts +173 -0
- package/tsconfig.json +19 -0
- package/agents/profiles.json +0 -5
- package/docs/CLI-REFERENCE.md +0 -361
- package/lib/assets/index.ts +0 -225
- package/lib/assets.ts +0 -124
- package/lib/auth/index.ts +0 -189
- package/lib/board/index.ts +0 -309
- package/lib/board/types.ts +0 -124
- package/lib/bot/claim.ts +0 -43
- package/lib/bot/coordinator.ts +0 -254
- package/lib/bot/heartbeat.ts +0 -37
- package/lib/bot/index.ts +0 -9
- package/lib/bot/protocol.ts +0 -99
- package/lib/bot/reporter.ts +0 -42
- package/lib/bot/skills.ts +0 -81
- package/lib/errors.ts +0 -129
- package/lib/format.ts +0 -120
- package/lib/returnpro/validate.ts +0 -154
- package/lib/social/meta.ts +0 -228
package/bin/optimal.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import 'dotenv/config'
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
getBoard,
|
|
6
|
+
createTask,
|
|
7
|
+
updateTask,
|
|
8
|
+
logActivity,
|
|
9
|
+
type Task,
|
|
10
|
+
} from '../lib/kanban.js'
|
|
11
|
+
import {
|
|
12
|
+
getBoardByProject,
|
|
13
|
+
createTask as createObsidianTask,
|
|
14
|
+
updateTaskStatus as updateObsidianTaskStatus,
|
|
15
|
+
loadAllTasks,
|
|
16
|
+
type BoardColumn,
|
|
17
|
+
type ObsidianTask,
|
|
18
|
+
} from '../lib/kanban-obsidian.js'
|
|
14
19
|
import { runAuditComparison } from '../lib/returnpro/audit.js'
|
|
15
20
|
import { exportKpis, formatKpiTable, formatKpiCsv } from '../lib/returnpro/kpis.js'
|
|
16
21
|
import { deploy, healthCheck, listApps } from '../lib/infra/deploy.js'
|
|
@@ -38,8 +43,6 @@ import { distributeNewsletter, checkDistributionStatus } from '../lib/newsletter
|
|
|
38
43
|
import { generateSocialPosts } from '../lib/social/post-generator.js'
|
|
39
44
|
import { publishSocialPosts, getPublishQueue, retryFailed } from '../lib/social/publish.js'
|
|
40
45
|
import { publishBlog, createBlogPost, listBlogDrafts } from '../lib/cms/publish-blog.js'
|
|
41
|
-
import { publishIgPhoto, getMetaConfigForBrand } from '../lib/social/meta.js'
|
|
42
|
-
import { strapiGet, strapiPut, type StrapiPage } from '../lib/cms/strapi-client.js'
|
|
43
46
|
import { migrateDb, listPendingMigrations, createMigration } from '../lib/infra/migrate.js'
|
|
44
47
|
import { saveScenario, loadScenario, listScenarios, compareScenarios, deleteScenario } from '../lib/budget/scenarios.js'
|
|
45
48
|
import { deleteBatch, previewBatch } from '../lib/transactions/delete-batch.js'
|
|
@@ -54,271 +57,318 @@ import {
|
|
|
54
57
|
readLocalConfig,
|
|
55
58
|
writeLocalConfig,
|
|
56
59
|
} from '../lib/config/registry.js'
|
|
57
|
-
import {
|
|
58
|
-
sendHeartbeat, getActiveAgents,
|
|
59
|
-
claimNextTask, releaseTask,
|
|
60
|
-
reportProgress, reportCompletion, reportBlocked,
|
|
61
|
-
runCoordinatorLoop, getCoordinatorStatus, assignTask, rebalance,
|
|
62
|
-
} from '../lib/bot/index.js'
|
|
63
|
-
import {
|
|
64
|
-
colorize, table as fmtTable, statusBadge, priorityBadge,
|
|
65
|
-
success, error as fmtError, warn as fmtWarn, info as fmtInfo,
|
|
66
|
-
} from '../lib/format.js'
|
|
67
|
-
import {
|
|
68
|
-
listAssets, createAsset, updateAsset, getAsset, deleteAsset,
|
|
69
|
-
trackAssetUsage, listAssetUsage, formatAssetTable,
|
|
70
|
-
type AssetType, type AssetStatus,
|
|
71
|
-
} from '../lib/assets/index.js'
|
|
72
60
|
|
|
73
61
|
const program = new Command()
|
|
74
62
|
.name('optimal')
|
|
75
63
|
.description('Optimal CLI — unified skills for financial analytics, content, and infra')
|
|
76
64
|
.version('0.1.0')
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
$ optimal board view -s in_progress Filter board by status
|
|
81
|
-
$ optimal board claim --id <uuid> --agent bot1 Claim a task
|
|
82
|
-
$ optimal project list List all projects
|
|
83
|
-
$ optimal publish-instagram --brand CRE-11TRUST Publish to Instagram
|
|
84
|
-
$ optimal social-queue --brand CRE-11TRUST View social post queue
|
|
85
|
-
$ optimal generate-newsletter --brand CRE-11TRUST Generate a newsletter
|
|
86
|
-
$ optimal audit-financials --months 2025-01 Audit a single month
|
|
87
|
-
$ optimal export-kpis --format csv > kpis.csv Export KPIs as CSV
|
|
88
|
-
$ optimal deploy dashboard --prod Deploy to production
|
|
89
|
-
$ optimal bot agents List active bot agents
|
|
90
|
-
$ optimal config doctor Validate local config
|
|
91
|
-
`)
|
|
92
|
-
|
|
93
|
-
// --- Board commands ---
|
|
94
|
-
const board = program.command('board').description('Kanban board operations')
|
|
95
|
-
.addHelpText('after', `
|
|
96
|
-
Examples:
|
|
97
|
-
$ optimal board view Show full board
|
|
98
|
-
$ optimal board view -p cli-consolidation Filter by project
|
|
99
|
-
$ optimal board view -s ready --mine bot1 Show bot1's ready tasks
|
|
100
|
-
$ optimal board create -t "Fix bug" -p cli-consolidation
|
|
101
|
-
$ optimal board claim --id <uuid> --agent bot1
|
|
102
|
-
$ optimal board log --actor bot1 --limit 5
|
|
103
|
-
`)
|
|
65
|
+
|
|
66
|
+
// Board commands (supabase-backed)
|
|
67
|
+
const board = program.command('board').description('Kanban board operations (supabase)')
|
|
104
68
|
|
|
105
69
|
board
|
|
106
70
|
.command('view')
|
|
107
71
|
.description('Display the kanban board')
|
|
108
|
-
.option('-p, --project <slug>', 'Project slug')
|
|
72
|
+
.option('-p, --project <slug>', 'Project slug', 'optimal-cli')
|
|
109
73
|
.option('-s, --status <status>', 'Filter by status')
|
|
110
|
-
.option('--mine <agent>', 'Show only tasks claimed by agent')
|
|
111
74
|
.action(async (opts) => {
|
|
112
|
-
|
|
113
|
-
if (opts.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
75
|
+
let tasks = await getBoard(opts.project)
|
|
76
|
+
if (opts.status) tasks = tasks.filter(t => t.status === opts.status)
|
|
77
|
+
|
|
78
|
+
const grouped = new Map<string, Task[]>()
|
|
79
|
+
for (const t of tasks) {
|
|
80
|
+
const list = grouped.get(t.status) ?? []
|
|
81
|
+
list.push(t)
|
|
82
|
+
grouped.set(t.status, list)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const order = ['in_progress', 'blocked', 'ready', 'backlog', 'review', 'done']
|
|
86
|
+
console.log('| Status | P | Title | Claimed By |')
|
|
87
|
+
console.log('|--------|---|-------|------------|')
|
|
88
|
+
for (const status of order) {
|
|
89
|
+
const list = grouped.get(status) ?? []
|
|
90
|
+
for (const t of list) {
|
|
91
|
+
console.log(
|
|
92
|
+
`| ${t.status} | ${t.priority} | ${t.title} | ${t.claimed_by ?? '—'} |`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log(`\nTotal: ${tasks.length} tasks`)
|
|
121
97
|
})
|
|
122
98
|
|
|
123
99
|
board
|
|
124
100
|
.command('create')
|
|
125
101
|
.description('Create a new task')
|
|
126
|
-
.addHelpText('after', `
|
|
127
|
-
Example:
|
|
128
|
-
$ optimal board create -t "Migrate auth" -p cli-consolidation --priority 1 --labels infra,migration
|
|
129
|
-
`)
|
|
130
102
|
.requiredOption('-t, --title <title>', 'Task title')
|
|
131
|
-
.
|
|
103
|
+
.option('-p, --project <slug>', 'Project slug', 'optimal-cli')
|
|
132
104
|
.option('-d, --description <desc>', 'Task description')
|
|
133
105
|
.option('--priority <n>', 'Priority 1-4', '3')
|
|
134
|
-
.option('--skill <ref>', 'Skill
|
|
106
|
+
.option('--skill <ref>', 'Skill required')
|
|
135
107
|
.option('--source <repo>', 'Source repo')
|
|
136
|
-
.option('--target <module>', 'Target module')
|
|
137
|
-
.option('--effort <size>', 'Effort: xs, s, m, l, xl')
|
|
138
|
-
.option('--blocked-by <ids>', 'Comma-separated blocking task IDs')
|
|
139
|
-
.option('--labels <labels>', 'Comma-separated labels')
|
|
140
108
|
.action(async (opts) => {
|
|
141
|
-
const project = await getProjectBySlug(opts.project)
|
|
142
109
|
const task = await createTask({
|
|
143
|
-
|
|
110
|
+
project_slug: opts.project,
|
|
144
111
|
title: opts.title,
|
|
145
112
|
description: opts.description,
|
|
146
|
-
priority: parseInt(opts.priority)
|
|
113
|
+
priority: parseInt(opts.priority),
|
|
147
114
|
skill_required: opts.skill,
|
|
148
115
|
source_repo: opts.source,
|
|
149
|
-
target_module: opts.target,
|
|
150
|
-
estimated_effort: opts.effort,
|
|
151
|
-
blocked_by: opts.blockedBy?.split(',') ?? [],
|
|
152
|
-
labels: opts.labels?.split(',') ?? [],
|
|
153
116
|
})
|
|
154
|
-
|
|
117
|
+
console.log(`Created task: ${task.id} — "${task.title}" (priority ${task.priority}, status ${task.status})`)
|
|
155
118
|
})
|
|
156
119
|
|
|
157
120
|
board
|
|
158
121
|
.command('update')
|
|
159
122
|
.description('Update a task')
|
|
160
|
-
.requiredOption('--id <
|
|
123
|
+
.requiredOption('--id <taskId>', 'Task UUID')
|
|
161
124
|
.option('-s, --status <status>', 'New status')
|
|
162
125
|
.option('-a, --agent <name>', 'Assign to agent')
|
|
163
126
|
.option('--priority <n>', 'New priority')
|
|
164
|
-
.option('-m, --message <msg>', 'Log message
|
|
127
|
+
.option('-m, --message <msg>', 'Log message')
|
|
165
128
|
.action(async (opts) => {
|
|
166
129
|
const updates: Record<string, unknown> = {}
|
|
167
130
|
if (opts.status) updates.status = opts.status
|
|
168
131
|
if (opts.agent) updates.assigned_to = opts.agent
|
|
169
132
|
if (opts.priority) updates.priority = parseInt(opts.priority)
|
|
170
|
-
|
|
171
|
-
const task = await updateTask(opts.id, updates
|
|
172
|
-
if (opts.message)
|
|
173
|
-
|
|
133
|
+
|
|
134
|
+
const task = await updateTask(opts.id, updates)
|
|
135
|
+
if (opts.message) {
|
|
136
|
+
await logActivity(opts.id, {
|
|
137
|
+
agent: opts.agent ?? 'cli',
|
|
138
|
+
action: 'status_change',
|
|
139
|
+
details: opts.message,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
console.log(`Updated task ${task.id}: status → ${task.status}, assigned_to → ${task.assigned_to ?? '—'}`)
|
|
174
143
|
})
|
|
175
144
|
|
|
176
145
|
board
|
|
177
146
|
.command('claim')
|
|
178
|
-
.description('Claim a task
|
|
179
|
-
.requiredOption('--id <
|
|
180
|
-
.
|
|
147
|
+
.description('Claim a task for an agent')
|
|
148
|
+
.requiredOption('--id <taskId>', 'Task UUID')
|
|
149
|
+
.option('-a, --agent <name>', 'Agent name', 'oracle')
|
|
181
150
|
.action(async (opts) => {
|
|
151
|
+
const { claimTask } = await import('../lib/kanban.js')
|
|
182
152
|
const task = await claimTask(opts.id, opts.agent)
|
|
183
|
-
|
|
153
|
+
console.log(`Claimed task: ${task.title} by ${task.claimed_by} (status: ${task.status})`)
|
|
184
154
|
})
|
|
185
155
|
|
|
186
156
|
board
|
|
187
|
-
.command('
|
|
188
|
-
.description('
|
|
189
|
-
.
|
|
190
|
-
.
|
|
191
|
-
.requiredOption('--body <text>', 'Comment body')
|
|
157
|
+
.command('my-tasks')
|
|
158
|
+
.description('Show tasks claimed by an agent')
|
|
159
|
+
.option('-a, --agent <name>', 'Agent name', 'oracle')
|
|
160
|
+
.option('-p, --project <slug>', 'Project slug', 'optimal-cli')
|
|
192
161
|
.action(async (opts) => {
|
|
193
|
-
const
|
|
194
|
-
|
|
162
|
+
const { getBoard } = await import('../lib/kanban.js')
|
|
163
|
+
const tasks = await getBoard(opts.project)
|
|
164
|
+
const myTasks = tasks.filter(t => t.claimed_by === opts.agent)
|
|
165
|
+
|
|
166
|
+
console.log(`\n📋 Tasks claimed by ${opts.agent}:\n`)
|
|
167
|
+
for (const t of myTasks) {
|
|
168
|
+
console.log(` [${t.status}] ${t.title} (priority ${t.priority})`)
|
|
169
|
+
}
|
|
170
|
+
console.log(`\nTotal: ${myTasks.length} tasks`)
|
|
195
171
|
})
|
|
196
172
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
202
|
-
.
|
|
173
|
+
// Sync commands for 3-way sync
|
|
174
|
+
const syncCmd = program.command('sync').description('Sync tasks between obsidian and supabase')
|
|
175
|
+
|
|
176
|
+
syncCmd
|
|
177
|
+
.command('push')
|
|
178
|
+
.description('Push obsidian tasks to supabase')
|
|
179
|
+
.option('--project <slug>', 'Target project slug', 'optimal-tasks')
|
|
180
|
+
.option('--dry-run', 'Show what would be synced without writing', false)
|
|
203
181
|
.action(async (opts) => {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
actor: opts.actor,
|
|
207
|
-
limit: parseInt(opts.limit),
|
|
208
|
-
})
|
|
209
|
-
for (const e of entries) {
|
|
210
|
-
console.log(`${e.created_at} | ${e.actor.padEnd(8)} | ${e.action.padEnd(15)} | ${JSON.stringify(e.new_value ?? {})}`)
|
|
211
|
-
}
|
|
212
|
-
console.log(`\n${entries.length} entries`)
|
|
182
|
+
const { syncObsidianToSupabase } = await import('../lib/kanban-sync.js')
|
|
183
|
+
await syncObsidianToSupabase(opts.project, opts.dryRun)
|
|
213
184
|
})
|
|
214
185
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
proj
|
|
225
|
-
.command('list')
|
|
226
|
-
.description('List all projects')
|
|
227
|
-
.action(async () => {
|
|
228
|
-
const projects = await listProjects()
|
|
229
|
-
console.log('| Status | P | Slug | Owner | Name |')
|
|
230
|
-
console.log('|----------|---|-------------------------|---------|------|')
|
|
231
|
-
for (const p of projects) {
|
|
232
|
-
console.log(`| ${p.status.padEnd(8)} | ${p.priority} | ${p.slug.padEnd(23)} | ${(p.owner ?? '—').padEnd(7)} | ${p.name} |`)
|
|
233
|
-
}
|
|
186
|
+
syncCmd
|
|
187
|
+
.command('pull')
|
|
188
|
+
.description('Pull supabase tasks to obsidian')
|
|
189
|
+
.option('--project <slug>', 'Source project slug', 'optimal-tasks')
|
|
190
|
+
.option('--dry-run', 'Show what would be synced without writing', false)
|
|
191
|
+
.action(async (opts) => {
|
|
192
|
+
const { syncSupabaseToObsidian } = await import('../lib/kanban-sync.js')
|
|
193
|
+
await syncSupabaseToObsidian(opts.project, opts.dryRun)
|
|
234
194
|
})
|
|
235
195
|
|
|
236
|
-
|
|
237
|
-
.command('
|
|
238
|
-
.description('
|
|
239
|
-
.
|
|
240
|
-
.requiredOption('--name <name>', 'Project name')
|
|
241
|
-
.option('--owner <name>', 'Owner')
|
|
242
|
-
.option('--priority <n>', 'Priority 1-4', '3')
|
|
196
|
+
syncCmd
|
|
197
|
+
.command('status')
|
|
198
|
+
.description('Show sync status between obsidian and supabase')
|
|
199
|
+
.option('--project <slug>', 'Project slug', 'optimal-tasks')
|
|
243
200
|
.action(async (opts) => {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
name: opts.name,
|
|
247
|
-
owner: opts.owner,
|
|
248
|
-
priority: parseInt(opts.priority) as 1 | 2 | 3 | 4,
|
|
249
|
-
})
|
|
250
|
-
success(`Created project: ${colorize(p.slug, 'cyan')} (${colorize(p.id, 'dim')})`)
|
|
201
|
+
const { getSyncStatus } = await import('../lib/kanban-sync.js')
|
|
202
|
+
await getSyncStatus(opts.project)
|
|
251
203
|
})
|
|
252
204
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
.
|
|
205
|
+
// Obsidian-backed board commands (primary)
|
|
206
|
+
const oboard = program.command('oboard').description('Obsidian Kanban board (primary)')
|
|
207
|
+
|
|
208
|
+
oboard
|
|
209
|
+
.command('view')
|
|
210
|
+
.description('Display the obsidian task board')
|
|
211
|
+
.option('-p, --project <slug>', 'Project filter')
|
|
212
|
+
.option('-s, --status <status>', 'Filter by status (pending|in-progress|done)')
|
|
259
213
|
.action(async (opts) => {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
if (opts.
|
|
263
|
-
|
|
264
|
-
|
|
214
|
+
const columns = await getBoardByProject(opts.project)
|
|
215
|
+
|
|
216
|
+
if (opts.status) {
|
|
217
|
+
const col = columns.find(c => c.status === opts.status)
|
|
218
|
+
if (col) {
|
|
219
|
+
console.log(`\n## ${col.name}\n`)
|
|
220
|
+
for (const task of col.tasks) {
|
|
221
|
+
console.log(`- [${task.priority}] ${task.title} (${task.assignee || 'unassigned'})`)
|
|
222
|
+
}
|
|
223
|
+
console.log(`\nTotal: ${col.tasks.length} tasks`)
|
|
224
|
+
}
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const col of columns) {
|
|
229
|
+
console.log(`\n## ${col.name} (${col.tasks.length})\n`)
|
|
230
|
+
for (const task of col.tasks) {
|
|
231
|
+
const tags = task.tags.length ? ` [${task.tags.join(', ')}]` : ''
|
|
232
|
+
console.log(`- [${task.priority}] ${task.title}${tags}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(`\nTotal: ${columns.reduce((sum, c) => sum + c.tasks.length, 0)} tasks`)
|
|
265
236
|
})
|
|
266
237
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
238
|
+
oboard
|
|
239
|
+
.command('list')
|
|
240
|
+
.description('List all tasks with details')
|
|
241
|
+
.option('-p, --project <slug>', 'Project filter')
|
|
242
|
+
.option('-s, --status <status>', 'Status filter')
|
|
243
|
+
.option('--assignee <name>', 'Filter by assignee')
|
|
244
|
+
.option('--owner <name>', 'Filter by owner')
|
|
245
|
+
.option('--json', 'Output as JSON (for agent consumption)', false)
|
|
246
|
+
.action(async (opts: { project?: string; status?: string; assignee?: string; owner?: string; json?: boolean }) => {
|
|
247
|
+
let tasks = await loadAllTasks()
|
|
248
|
+
|
|
249
|
+
if (opts.project) tasks = tasks.filter(t => t.project === opts.project)
|
|
250
|
+
if (opts.status) tasks = tasks.filter(t => t.status === opts.status)
|
|
251
|
+
if (opts.assignee) tasks = tasks.filter(t => t.assignee === opts.assignee)
|
|
252
|
+
if (opts.owner) tasks = tasks.filter(t => t.owner === opts.owner)
|
|
253
|
+
|
|
254
|
+
if (opts.json) {
|
|
255
|
+
// Agent-friendly JSON output
|
|
256
|
+
console.log(JSON.stringify(tasks.map(t => ({
|
|
257
|
+
id: t.id,
|
|
258
|
+
title: t.title,
|
|
259
|
+
status: t.status,
|
|
260
|
+
priority: t.priority,
|
|
261
|
+
owner: t.owner,
|
|
262
|
+
assignee: t.assignee,
|
|
263
|
+
project: t.project,
|
|
264
|
+
tags: t.tags,
|
|
265
|
+
updatedAt: t.updatedAt,
|
|
266
|
+
createdAt: t.createdAt
|
|
267
|
+
})), null, 2))
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(`\nFound ${tasks.length} tasks:\n`)
|
|
272
|
+
for (const task of tasks) {
|
|
273
|
+
console.log(`ID: ${task.id}`)
|
|
274
|
+
console.log(` Title: ${task.title}`)
|
|
275
|
+
console.log(` Status: ${task.status} | Priority: ${task.priority}`)
|
|
276
|
+
console.log(` Owner: ${task.owner || '—'} | Assignee: ${task.assignee || '—'}`)
|
|
277
|
+
console.log(` Project: ${task.project || '—'}`)
|
|
278
|
+
console.log(` Tags: ${task.tags.join(', ') || 'none'}`)
|
|
279
|
+
console.log(` Updated: ${task.updatedAt || 'never'}`)
|
|
280
|
+
console.log('')
|
|
281
|
+
}
|
|
282
|
+
})
|
|
274
283
|
|
|
275
|
-
|
|
284
|
+
oboard
|
|
276
285
|
.command('create')
|
|
277
|
-
.description('Create a
|
|
278
|
-
.requiredOption('--
|
|
279
|
-
.
|
|
280
|
-
.option('--
|
|
286
|
+
.description('Create a new obsidian task')
|
|
287
|
+
.requiredOption('-t, --title <title>', 'Task title')
|
|
288
|
+
.option('-d, --description <desc>', 'Task description')
|
|
289
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
290
|
+
.option('--priority <n>', 'Priority 1-4', '3')
|
|
291
|
+
.option('--owner <name>', 'Owner', 'oracle')
|
|
292
|
+
.option('--assignee <name>', 'Assignee')
|
|
293
|
+
.option('--tags <labels>', 'Comma-separated tags')
|
|
281
294
|
.action(async (opts) => {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
const result = await createObsidianTask({
|
|
296
|
+
title: opts.title,
|
|
297
|
+
description: opts.description,
|
|
298
|
+
project: opts.project,
|
|
299
|
+
priority: parseInt(opts.priority) || 3,
|
|
300
|
+
owner: opts.owner,
|
|
301
|
+
assignee: opts.assignee,
|
|
302
|
+
tags: opts.tags?.split(',').map((t: string) => t.trim()),
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
if (result.ok) {
|
|
306
|
+
console.log(`✓ ${result.message}`)
|
|
307
|
+
console.log(` ID: ${result.task?.id}`)
|
|
308
|
+
} else {
|
|
309
|
+
console.error(`✗ ${result.message}`)
|
|
310
|
+
process.exit(1)
|
|
311
|
+
}
|
|
285
312
|
})
|
|
286
313
|
|
|
287
|
-
|
|
288
|
-
.command('
|
|
289
|
-
.description('
|
|
290
|
-
.
|
|
314
|
+
oboard
|
|
315
|
+
.command('update')
|
|
316
|
+
.description('Update an obsidian task')
|
|
317
|
+
.requiredOption('--id <taskId>', 'Task ID (or partial match)')
|
|
318
|
+
.option('-s, --status <status>', 'New status (pending|in-progress|done)')
|
|
319
|
+
.option('-a, --assignee <name>', 'Assign to agent')
|
|
320
|
+
.option('--owner <name>', 'Set owner')
|
|
321
|
+
.option('--priority <n>', 'Set priority 1-4')
|
|
322
|
+
.option('-m, --message <msg>', 'Log progress message')
|
|
291
323
|
.action(async (opts) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
projectId = p.id
|
|
324
|
+
if (!opts.status && !opts.assignee && !opts.owner && !opts.priority && !opts.message) {
|
|
325
|
+
console.error('No updates specified. Use --status, --assignee, --owner, --priority, or --message')
|
|
326
|
+
process.exit(1)
|
|
296
327
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
|
|
329
|
+
if (opts.status) {
|
|
330
|
+
const result = await updateObsidianTaskStatus(opts.id, opts.status, opts.message)
|
|
331
|
+
if (result.ok) {
|
|
332
|
+
console.log(`✓ ${result.message}`)
|
|
333
|
+
} else {
|
|
334
|
+
console.error(`✗ ${result.message}`)
|
|
335
|
+
process.exit(1)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// TODO: Add other update fields
|
|
340
|
+
if (opts.assignee || opts.owner || opts.priority) {
|
|
341
|
+
console.log('Note: Full field updates not yet implemented, use status for now')
|
|
300
342
|
}
|
|
301
343
|
})
|
|
302
344
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
.
|
|
308
|
-
.description('Create a label')
|
|
309
|
-
.requiredOption('--name <name>', 'Label name')
|
|
310
|
-
.option('--color <hex>', 'Color hex code')
|
|
345
|
+
oboard
|
|
346
|
+
.command('done')
|
|
347
|
+
.description('Mark a task as done')
|
|
348
|
+
.requiredOption('--id <taskId>', 'Task ID (or partial match)')
|
|
349
|
+
.option('-m, --message <msg>', 'Completion message')
|
|
311
350
|
.action(async (opts) => {
|
|
312
|
-
const
|
|
313
|
-
|
|
351
|
+
const result = await updateObsidianTaskStatus(opts.id, 'done', opts.message)
|
|
352
|
+
if (result.ok) {
|
|
353
|
+
console.log(`✓ Completed: ${result.message}`)
|
|
354
|
+
} else {
|
|
355
|
+
console.error(`✗ ${result.message}`)
|
|
356
|
+
process.exit(1)
|
|
357
|
+
}
|
|
314
358
|
})
|
|
315
359
|
|
|
316
|
-
|
|
317
|
-
.command('
|
|
318
|
-
.description('
|
|
319
|
-
.
|
|
320
|
-
|
|
321
|
-
|
|
360
|
+
oboard
|
|
361
|
+
.command('start')
|
|
362
|
+
.description('Mark a task as in-progress')
|
|
363
|
+
.requiredOption('--id <taskId>', 'Task ID (or partial match)')
|
|
364
|
+
.action(async (opts) => {
|
|
365
|
+
const result = await updateObsidianTaskStatus(opts.id, 'in-progress')
|
|
366
|
+
if (result.ok) {
|
|
367
|
+
console.log(`✓ Started: ${result.message}`)
|
|
368
|
+
} else {
|
|
369
|
+
console.error(`✗ ${result.message}`)
|
|
370
|
+
process.exit(1)
|
|
371
|
+
}
|
|
322
372
|
})
|
|
323
373
|
|
|
324
374
|
// Audit financials command
|
|
@@ -426,13 +476,13 @@ program
|
|
|
426
476
|
.argument('<app>', `App to deploy (${listApps().join(', ')})`)
|
|
427
477
|
.option('--prod', 'Deploy to production', false)
|
|
428
478
|
.action(async (app: string, opts: { prod: boolean }) => {
|
|
429
|
-
|
|
479
|
+
console.log(`Deploying ${app}${opts.prod ? ' (production)' : ' (preview)'}...`)
|
|
430
480
|
try {
|
|
431
481
|
const url = await deploy(app, opts.prod)
|
|
432
|
-
|
|
482
|
+
console.log(`Deployed: ${url}`)
|
|
433
483
|
} catch (err) {
|
|
434
484
|
const msg = err instanceof Error ? err.message : String(err)
|
|
435
|
-
|
|
485
|
+
console.error(`Deploy failed: ${msg}`)
|
|
436
486
|
process.exit(1)
|
|
437
487
|
}
|
|
438
488
|
})
|
|
@@ -562,11 +612,11 @@ program
|
|
|
562
612
|
})
|
|
563
613
|
|
|
564
614
|
if (result.strapiDocumentId) {
|
|
565
|
-
|
|
615
|
+
console.log(`\nStrapi documentId: ${result.strapiDocumentId}`)
|
|
566
616
|
}
|
|
567
617
|
} catch (err) {
|
|
568
618
|
const msg = err instanceof Error ? err.message : String(err)
|
|
569
|
-
|
|
619
|
+
console.error(`Newsletter generation failed: ${msg}`)
|
|
570
620
|
process.exit(1)
|
|
571
621
|
}
|
|
572
622
|
})
|
|
@@ -956,78 +1006,6 @@ program
|
|
|
956
1006
|
console.log(`\n${queue.length} posts queued`)
|
|
957
1007
|
})
|
|
958
1008
|
|
|
959
|
-
// ── Publish to Instagram via Meta Graph API ─────────────────────────
|
|
960
|
-
program
|
|
961
|
-
.command('publish-instagram')
|
|
962
|
-
.description('Publish pending social posts to Instagram via Meta Graph API')
|
|
963
|
-
.requiredOption('--brand <brand>', 'Brand: CRE-11TRUST or LIFEINSUR')
|
|
964
|
-
.option('--limit <n>', 'Max posts to publish')
|
|
965
|
-
.option('--dry-run', 'Preview without publishing', false)
|
|
966
|
-
.action(async (opts: { brand: string; limit?: string; dryRun: boolean }) => {
|
|
967
|
-
try {
|
|
968
|
-
const config = getMetaConfigForBrand(opts.brand)
|
|
969
|
-
|
|
970
|
-
// Fetch pending instagram posts from Strapi
|
|
971
|
-
const result = await strapiGet<StrapiPage>('/api/social-posts', {
|
|
972
|
-
'filters[brand][$eq]': opts.brand,
|
|
973
|
-
'filters[delivery_status][$eq]': 'pending',
|
|
974
|
-
'filters[platform][$eq]': 'instagram',
|
|
975
|
-
'sort': 'scheduled_date:asc',
|
|
976
|
-
'pagination[pageSize]': opts.limit ?? '50',
|
|
977
|
-
})
|
|
978
|
-
|
|
979
|
-
const posts = result.data
|
|
980
|
-
if (posts.length === 0) {
|
|
981
|
-
console.log('No pending Instagram posts found')
|
|
982
|
-
return
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
console.log(`Found ${posts.length} pending Instagram post(s) for ${opts.brand}`)
|
|
986
|
-
let published = 0
|
|
987
|
-
let failed = 0
|
|
988
|
-
|
|
989
|
-
for (const post of posts) {
|
|
990
|
-
const headline = (post.headline as string) ?? '(no headline)'
|
|
991
|
-
const imageUrl = post.image_url as string | undefined
|
|
992
|
-
const caption = ((post.body as string) ?? (post.headline as string) ?? '').trim()
|
|
993
|
-
|
|
994
|
-
if (!imageUrl) {
|
|
995
|
-
console.log(` SKIP | ${headline} — no image_url`)
|
|
996
|
-
failed++
|
|
997
|
-
continue
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
if (opts.dryRun) {
|
|
1001
|
-
console.log(` DRY | ${headline}`)
|
|
1002
|
-
continue
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
try {
|
|
1006
|
-
const igResult = await publishIgPhoto(config, { imageUrl, caption })
|
|
1007
|
-
await strapiPut('/api/social-posts', post.documentId, {
|
|
1008
|
-
delivery_status: 'delivered',
|
|
1009
|
-
platform_post_id: igResult.mediaId,
|
|
1010
|
-
})
|
|
1011
|
-
console.log(` OK | ${headline} → ${igResult.mediaId}`)
|
|
1012
|
-
published++
|
|
1013
|
-
} catch (err) {
|
|
1014
|
-
const errMsg = err instanceof Error ? err.message : String(err)
|
|
1015
|
-
await strapiPut('/api/social-posts', post.documentId, {
|
|
1016
|
-
delivery_status: 'failed',
|
|
1017
|
-
delivery_errors: [{ timestamp: new Date().toISOString(), error: errMsg }],
|
|
1018
|
-
}).catch(() => {})
|
|
1019
|
-
console.log(` FAIL | ${headline} — ${errMsg}`)
|
|
1020
|
-
failed++
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
console.log(`\nPublished: ${published} | Failed: ${failed}${opts.dryRun ? ' | (dry run)' : ''}`)
|
|
1025
|
-
} catch (err) {
|
|
1026
|
-
console.error(`Instagram publish failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
1027
|
-
process.exit(1)
|
|
1028
|
-
}
|
|
1029
|
-
})
|
|
1030
|
-
|
|
1031
1009
|
// ── Publish blog ────────────────────────────────────────────────────
|
|
1032
1010
|
program
|
|
1033
1011
|
.command('publish-blog')
|
|
@@ -1065,12 +1043,6 @@ program
|
|
|
1065
1043
|
|
|
1066
1044
|
// ── Database migration ──────────────────────────────────────────────
|
|
1067
1045
|
const migrate = program.command('migrate').description('Supabase database migration operations')
|
|
1068
|
-
.addHelpText('after', `
|
|
1069
|
-
Examples:
|
|
1070
|
-
$ optimal migrate pending --target optimalos
|
|
1071
|
-
$ optimal migrate push --target returnpro --dry-run
|
|
1072
|
-
$ optimal migrate create --target optimalos --name "add-index"
|
|
1073
|
-
`)
|
|
1074
1046
|
|
|
1075
1047
|
migrate
|
|
1076
1048
|
.command('push')
|
|
@@ -1119,13 +1091,6 @@ migrate
|
|
|
1119
1091
|
|
|
1120
1092
|
// ── Budget scenarios ────────────────────────────────────────────────
|
|
1121
1093
|
const scenario = program.command('scenario').description('Budget scenario management')
|
|
1122
|
-
.addHelpText('after', `
|
|
1123
|
-
Examples:
|
|
1124
|
-
$ optimal scenario list
|
|
1125
|
-
$ optimal scenario save --name "4pct-growth" --adjustment-type percentage --adjustment-value 4
|
|
1126
|
-
$ optimal scenario compare --names "baseline,4pct-growth"
|
|
1127
|
-
$ optimal scenario delete --name "old-scenario"
|
|
1128
|
-
`)
|
|
1129
1094
|
|
|
1130
1095
|
scenario
|
|
1131
1096
|
.command('save')
|
|
@@ -1265,15 +1230,6 @@ program
|
|
|
1265
1230
|
|
|
1266
1231
|
// ── Config registry (v1 scaffold) ─────────────────────────────────
|
|
1267
1232
|
const config = program.command('config').description('Manage optimal-cli local/shared config profile')
|
|
1268
|
-
.addHelpText('after', `
|
|
1269
|
-
Examples:
|
|
1270
|
-
$ optimal config init --owner oracle --brand CRE-11TRUST
|
|
1271
|
-
$ optimal config doctor
|
|
1272
|
-
$ optimal config export --out ./backup.json
|
|
1273
|
-
$ optimal config import --in ./backup.json
|
|
1274
|
-
$ optimal config sync pull
|
|
1275
|
-
$ optimal config sync push --agent bot1
|
|
1276
|
-
`)
|
|
1277
1233
|
|
|
1278
1234
|
config
|
|
1279
1235
|
.command('init')
|
|
@@ -1410,7 +1366,15 @@ configSync
|
|
|
1410
1366
|
.command('pull')
|
|
1411
1367
|
.description('Pull config profile from shared registry into local config')
|
|
1412
1368
|
.option('--profile <name>', 'Registry profile name', 'default')
|
|
1413
|
-
.
|
|
1369
|
+
.option('--dry-run', 'Show what would be pulled without writing', false)
|
|
1370
|
+
.action(async (opts: { profile: string; dryRun?: boolean }) => {
|
|
1371
|
+
if (opts.dryRun) {
|
|
1372
|
+
const local = await readLocalConfig()
|
|
1373
|
+
const owner = local?.profile.owner || process.env.OPTIMAL_CONFIG_OWNER || 'unknown'
|
|
1374
|
+
console.log(`[dry-run] Would pull profile '${opts.profile}' for owner '${owner}' from registry`)
|
|
1375
|
+
console.log(`[dry-run] Local config: ${local ? 'exists' : 'none'}`)
|
|
1376
|
+
return
|
|
1377
|
+
}
|
|
1414
1378
|
const result = await pullRegistryProfile(opts.profile)
|
|
1415
1379
|
const stamp = new Date().toISOString()
|
|
1416
1380
|
await appendHistory(`${stamp} sync.pull profile=${opts.profile} ok=${result.ok} msg=${result.message}`)
|
|
@@ -1427,7 +1391,20 @@ configSync
|
|
|
1427
1391
|
.requiredOption('--agent <name>', 'Agent/owner name for the config profile')
|
|
1428
1392
|
.option('--profile <name>', 'Registry profile name', 'default')
|
|
1429
1393
|
.option('--force', 'Force write even on conflict', false)
|
|
1430
|
-
.
|
|
1394
|
+
.option('--dry-run', 'Show what would be pushed without writing', false)
|
|
1395
|
+
.action(async (opts: { agent: string; profile: string; force?: boolean; dryRun?: boolean }) => {
|
|
1396
|
+
if (opts.dryRun) {
|
|
1397
|
+
const local = await readLocalConfig()
|
|
1398
|
+
if (!local) {
|
|
1399
|
+
console.error('[dry-run] No local config to push')
|
|
1400
|
+
process.exit(1)
|
|
1401
|
+
}
|
|
1402
|
+
const hash = hashConfig(local)
|
|
1403
|
+
console.log(`[dry-run] Would push profile '${opts.profile}' for agent '${opts.agent}' to registry`)
|
|
1404
|
+
console.log(`[dry-run] Local config hash: ${hash}`)
|
|
1405
|
+
console.log(`[dry-run] Force: ${Boolean(opts.force)}`)
|
|
1406
|
+
return
|
|
1407
|
+
}
|
|
1431
1408
|
const result = await pushRegistryProfile(opts.profile, Boolean(opts.force), opts.agent)
|
|
1432
1409
|
const stamp = new Date().toISOString()
|
|
1433
1410
|
await appendHistory(`${stamp} sync.push agent=${opts.agent} profile=${opts.profile} force=${Boolean(opts.force)} ok=${result.ok} msg=${result.message}`)
|
|
@@ -1438,294 +1415,4 @@ configSync
|
|
|
1438
1415
|
console.log(result.message)
|
|
1439
1416
|
})
|
|
1440
1417
|
|
|
1441
|
-
// --- Bot commands ---
|
|
1442
|
-
const bot = program.command('bot').description('Bot agent orchestration')
|
|
1443
|
-
.addHelpText('after', `
|
|
1444
|
-
Examples:
|
|
1445
|
-
$ optimal bot agents List active agents
|
|
1446
|
-
$ optimal bot heartbeat --agent bot1 Send heartbeat
|
|
1447
|
-
$ optimal bot claim --agent bot1 Claim next task
|
|
1448
|
-
$ optimal bot report --task <id> --agent bot1 --message "50% done"
|
|
1449
|
-
$ optimal bot complete --task <id> --agent bot1 --summary "All tests pass"
|
|
1450
|
-
`)
|
|
1451
|
-
|
|
1452
|
-
bot
|
|
1453
|
-
.command('heartbeat')
|
|
1454
|
-
.description('Send agent heartbeat')
|
|
1455
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1456
|
-
.option('--status <s>', 'Status: idle, working, error', 'idle')
|
|
1457
|
-
.action(async (opts) => {
|
|
1458
|
-
await sendHeartbeat(opts.agent, opts.status as 'idle' | 'working' | 'error')
|
|
1459
|
-
success(`Heartbeat sent: ${colorize(opts.agent, 'bold')} [${colorize(opts.status, 'cyan')}]`)
|
|
1460
|
-
})
|
|
1461
|
-
|
|
1462
|
-
bot
|
|
1463
|
-
.command('agents')
|
|
1464
|
-
.description('List active agents (heartbeat in last 5 min)')
|
|
1465
|
-
.action(async () => {
|
|
1466
|
-
const agents = await getActiveAgents()
|
|
1467
|
-
if (agents.length === 0) {
|
|
1468
|
-
console.log('No active agents.')
|
|
1469
|
-
return
|
|
1470
|
-
}
|
|
1471
|
-
console.log('| Agent | Status | Last Seen |')
|
|
1472
|
-
console.log('|------------------|---------|---------------------|')
|
|
1473
|
-
for (const a of agents) {
|
|
1474
|
-
console.log(`| ${a.agent.padEnd(16)} | ${a.status.padEnd(7)} | ${a.lastSeen} |`)
|
|
1475
|
-
}
|
|
1476
|
-
})
|
|
1477
|
-
|
|
1478
|
-
bot
|
|
1479
|
-
.command('claim')
|
|
1480
|
-
.description('Claim the next available task')
|
|
1481
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1482
|
-
.option('--skill <s>', 'Skill filter (comma-separated)')
|
|
1483
|
-
.action(async (opts) => {
|
|
1484
|
-
const skills = opts.skill ? opts.skill.split(',') : undefined
|
|
1485
|
-
const task = await claimNextTask(opts.agent, skills)
|
|
1486
|
-
if (!task) {
|
|
1487
|
-
console.log('No claimable tasks found.')
|
|
1488
|
-
return
|
|
1489
|
-
}
|
|
1490
|
-
success(`Claimed: ${colorize(task.title, 'cyan')} (${colorize(task.id, 'dim')}) by ${colorize(opts.agent, 'bold')}`)
|
|
1491
|
-
})
|
|
1492
|
-
|
|
1493
|
-
bot
|
|
1494
|
-
.command('report')
|
|
1495
|
-
.description('Report progress on a task')
|
|
1496
|
-
.requiredOption('--task <id>', 'Task ID')
|
|
1497
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1498
|
-
.requiredOption('--message <msg>', 'Progress message')
|
|
1499
|
-
.action(async (opts) => {
|
|
1500
|
-
await reportProgress(opts.task, opts.agent, opts.message)
|
|
1501
|
-
success(`Progress reported on ${colorize(opts.task, 'dim')}`)
|
|
1502
|
-
})
|
|
1503
|
-
|
|
1504
|
-
bot
|
|
1505
|
-
.command('complete')
|
|
1506
|
-
.description('Mark a task as done')
|
|
1507
|
-
.requiredOption('--task <id>', 'Task ID')
|
|
1508
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1509
|
-
.requiredOption('--summary <s>', 'Completion summary')
|
|
1510
|
-
.action(async (opts) => {
|
|
1511
|
-
await reportCompletion(opts.task, opts.agent, opts.summary)
|
|
1512
|
-
success(`Task ${colorize(opts.task, 'dim')} marked ${statusBadge('done')} by ${colorize(opts.agent, 'bold')}`)
|
|
1513
|
-
})
|
|
1514
|
-
|
|
1515
|
-
bot
|
|
1516
|
-
.command('release')
|
|
1517
|
-
.description('Release a claimed task back to ready')
|
|
1518
|
-
.requiredOption('--task <id>', 'Task ID')
|
|
1519
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1520
|
-
.option('--reason <r>', 'Release reason')
|
|
1521
|
-
.action(async (opts) => {
|
|
1522
|
-
await releaseTask(opts.task, opts.agent, opts.reason)
|
|
1523
|
-
fmtInfo(`Task ${colorize(opts.task, 'dim')} released by ${colorize(opts.agent, 'bold')}`)
|
|
1524
|
-
})
|
|
1525
|
-
|
|
1526
|
-
bot
|
|
1527
|
-
.command('blocked')
|
|
1528
|
-
.description('Mark a task as blocked')
|
|
1529
|
-
.requiredOption('--task <id>', 'Task ID')
|
|
1530
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1531
|
-
.requiredOption('--reason <r>', 'Block reason')
|
|
1532
|
-
.action(async (opts) => {
|
|
1533
|
-
await reportBlocked(opts.task, opts.agent, opts.reason)
|
|
1534
|
-
fmtWarn(`Task ${colorize(opts.task, 'dim')} marked ${statusBadge('blocked')}: ${opts.reason}`)
|
|
1535
|
-
})
|
|
1536
|
-
|
|
1537
|
-
// --- Coordinator commands ---
|
|
1538
|
-
const coordinator = program.command('coordinator').description('Multi-agent coordination')
|
|
1539
|
-
.addHelpText('after', `
|
|
1540
|
-
Examples:
|
|
1541
|
-
$ optimal coordinator start Run coordinator loop
|
|
1542
|
-
$ optimal coordinator start --interval 10000 Poll every 10s
|
|
1543
|
-
$ optimal coordinator status Show coordinator status
|
|
1544
|
-
$ optimal coordinator assign --task <id> --agent bot1
|
|
1545
|
-
$ optimal coordinator rebalance Release stale tasks
|
|
1546
|
-
`)
|
|
1547
|
-
|
|
1548
|
-
coordinator
|
|
1549
|
-
.command('start')
|
|
1550
|
-
.description('Run the coordinator loop')
|
|
1551
|
-
.option('--interval <ms>', 'Poll interval in milliseconds', '30000')
|
|
1552
|
-
.option('--max-agents <n>', 'Maximum agents to manage', '10')
|
|
1553
|
-
.action(async (opts) => {
|
|
1554
|
-
await runCoordinatorLoop({
|
|
1555
|
-
pollIntervalMs: parseInt(opts.interval),
|
|
1556
|
-
maxAgents: parseInt(opts.maxAgents),
|
|
1557
|
-
})
|
|
1558
|
-
})
|
|
1559
|
-
|
|
1560
|
-
coordinator
|
|
1561
|
-
.command('status')
|
|
1562
|
-
.description('Show coordinator status')
|
|
1563
|
-
.action(async () => {
|
|
1564
|
-
const s = await getCoordinatorStatus()
|
|
1565
|
-
console.log(`Last poll: ${s.lastPollAt ?? 'never'}`)
|
|
1566
|
-
console.log(`Tasks — ready: ${s.tasksReady}, in progress: ${s.tasksInProgress}, blocked: ${s.tasksBlocked}`)
|
|
1567
|
-
console.log(`\nActive agents (${s.activeAgents.length}):`)
|
|
1568
|
-
for (const a of s.activeAgents) {
|
|
1569
|
-
console.log(` ${a.agent.padEnd(16)} ${a.status.padEnd(8)} last seen ${a.lastSeen}`)
|
|
1570
|
-
}
|
|
1571
|
-
console.log(`\nIdle agents (${s.idleAgents.length}):`)
|
|
1572
|
-
for (const a of s.idleAgents) {
|
|
1573
|
-
console.log(` ${a.id.padEnd(16)} skills: ${a.skills.join(', ')}`)
|
|
1574
|
-
}
|
|
1575
|
-
})
|
|
1576
|
-
|
|
1577
|
-
coordinator
|
|
1578
|
-
.command('assign')
|
|
1579
|
-
.description('Manually assign a task to an agent')
|
|
1580
|
-
.requiredOption('--task <id>', 'Task ID')
|
|
1581
|
-
.requiredOption('--agent <id>', 'Agent ID')
|
|
1582
|
-
.action(async (opts) => {
|
|
1583
|
-
const task = await assignTask(opts.task, opts.agent)
|
|
1584
|
-
success(`Assigned: ${colorize(task.title, 'cyan')} -> ${colorize(opts.agent, 'bold')}`)
|
|
1585
|
-
})
|
|
1586
|
-
|
|
1587
|
-
coordinator
|
|
1588
|
-
.command('rebalance')
|
|
1589
|
-
.description('Release stale tasks and rebalance')
|
|
1590
|
-
.action(async () => {
|
|
1591
|
-
const result = await rebalance()
|
|
1592
|
-
if (result.releasedTasks.length === 0) {
|
|
1593
|
-
fmtInfo('No stale tasks found.')
|
|
1594
|
-
return
|
|
1595
|
-
}
|
|
1596
|
-
console.log(`Released ${result.releasedTasks.length} stale task(s):`)
|
|
1597
|
-
for (const t of result.releasedTasks) {
|
|
1598
|
-
console.log(` ${colorize(t.id, 'dim')} ${t.title}`)
|
|
1599
|
-
}
|
|
1600
|
-
if (result.reassignedTasks.length > 0) {
|
|
1601
|
-
console.log(`Reassigned ${result.reassignedTasks.length} task(s):`)
|
|
1602
|
-
for (const t of result.reassignedTasks) {
|
|
1603
|
-
console.log(` ${colorize(t.id, 'dim')} ${t.title} -> ${t.claimed_by}`)
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
})
|
|
1607
|
-
|
|
1608
|
-
// --- Asset commands ---
|
|
1609
|
-
const asset = program.command('asset').description('Digital asset tracking (domains, servers, API keys, services, repos)')
|
|
1610
|
-
.addHelpText('after', `
|
|
1611
|
-
Examples:
|
|
1612
|
-
$ optimal asset list List all assets
|
|
1613
|
-
$ optimal asset list --type domain --status active Filter by type/status
|
|
1614
|
-
$ optimal asset add --name "op-hub.com" --type domain --owner clenisa
|
|
1615
|
-
$ optimal asset update --id <uuid> --status inactive
|
|
1616
|
-
$ optimal asset usage --id <uuid> View usage log
|
|
1617
|
-
`)
|
|
1618
|
-
|
|
1619
|
-
asset
|
|
1620
|
-
.command('list')
|
|
1621
|
-
.description('List tracked assets')
|
|
1622
|
-
.option('-t, --type <type>', 'Filter by type (domain, server, api_key, service, repo, other)')
|
|
1623
|
-
.option('-s, --status <status>', 'Filter by status (active, inactive, expired, pending)')
|
|
1624
|
-
.option('-o, --owner <owner>', 'Filter by owner')
|
|
1625
|
-
.option('--json', 'Output as JSON')
|
|
1626
|
-
.action(async (opts) => {
|
|
1627
|
-
const assets = await listAssets({
|
|
1628
|
-
type: opts.type as AssetType | undefined,
|
|
1629
|
-
status: opts.status as AssetStatus | undefined,
|
|
1630
|
-
owner: opts.owner,
|
|
1631
|
-
})
|
|
1632
|
-
if (opts.json) {
|
|
1633
|
-
console.log(JSON.stringify(assets, null, 2))
|
|
1634
|
-
} else {
|
|
1635
|
-
console.log(formatAssetTable(assets))
|
|
1636
|
-
}
|
|
1637
|
-
})
|
|
1638
|
-
|
|
1639
|
-
asset
|
|
1640
|
-
.command('add')
|
|
1641
|
-
.description('Add a new asset')
|
|
1642
|
-
.requiredOption('-n, --name <name>', 'Asset name')
|
|
1643
|
-
.requiredOption('-t, --type <type>', 'Asset type (domain, server, api_key, service, repo, other)')
|
|
1644
|
-
.option('-s, --status <status>', 'Status (default: active)')
|
|
1645
|
-
.option('-o, --owner <owner>', 'Owner')
|
|
1646
|
-
.option('--expires <date>', 'Expiration date (YYYY-MM-DD or ISO)')
|
|
1647
|
-
.option('--meta <json>', 'Metadata JSON string')
|
|
1648
|
-
.action(async (opts) => {
|
|
1649
|
-
const metadata = opts.meta ? JSON.parse(opts.meta) : undefined
|
|
1650
|
-
const created = await createAsset({
|
|
1651
|
-
name: opts.name,
|
|
1652
|
-
type: opts.type as AssetType,
|
|
1653
|
-
status: opts.status as AssetStatus | undefined,
|
|
1654
|
-
owner: opts.owner,
|
|
1655
|
-
expires_at: opts.expires,
|
|
1656
|
-
metadata,
|
|
1657
|
-
})
|
|
1658
|
-
success(`Created asset: ${colorize(created.name, 'cyan')} [${created.type}] (${colorize(created.id, 'dim')})`)
|
|
1659
|
-
})
|
|
1660
|
-
|
|
1661
|
-
asset
|
|
1662
|
-
.command('update')
|
|
1663
|
-
.description('Update an existing asset')
|
|
1664
|
-
.requiredOption('--id <uuid>', 'Asset ID')
|
|
1665
|
-
.option('-n, --name <name>', 'New name')
|
|
1666
|
-
.option('-t, --type <type>', 'New type')
|
|
1667
|
-
.option('-s, --status <status>', 'New status')
|
|
1668
|
-
.option('-o, --owner <owner>', 'New owner')
|
|
1669
|
-
.option('--expires <date>', 'New expiration date')
|
|
1670
|
-
.option('--meta <json>', 'New metadata JSON')
|
|
1671
|
-
.action(async (opts) => {
|
|
1672
|
-
const updates: Record<string, unknown> = {}
|
|
1673
|
-
if (opts.name) updates.name = opts.name
|
|
1674
|
-
if (opts.type) updates.type = opts.type
|
|
1675
|
-
if (opts.status) updates.status = opts.status
|
|
1676
|
-
if (opts.owner) updates.owner = opts.owner
|
|
1677
|
-
if (opts.expires) updates.expires_at = opts.expires
|
|
1678
|
-
if (opts.meta) updates.metadata = JSON.parse(opts.meta)
|
|
1679
|
-
const updated = await updateAsset(opts.id, updates)
|
|
1680
|
-
success(`Updated: ${colorize(updated.name, 'cyan')} -> status=${colorize(updated.status, 'bold')}`)
|
|
1681
|
-
})
|
|
1682
|
-
|
|
1683
|
-
asset
|
|
1684
|
-
.command('get')
|
|
1685
|
-
.description('Get a single asset by ID')
|
|
1686
|
-
.requiredOption('--id <uuid>', 'Asset ID')
|
|
1687
|
-
.action(async (opts) => {
|
|
1688
|
-
const a = await getAsset(opts.id)
|
|
1689
|
-
console.log(JSON.stringify(a, null, 2))
|
|
1690
|
-
})
|
|
1691
|
-
|
|
1692
|
-
asset
|
|
1693
|
-
.command('remove')
|
|
1694
|
-
.description('Delete an asset')
|
|
1695
|
-
.requiredOption('--id <uuid>', 'Asset ID')
|
|
1696
|
-
.action(async (opts) => {
|
|
1697
|
-
await deleteAsset(opts.id)
|
|
1698
|
-
success(`Deleted asset ${colorize(opts.id, 'dim')}`)
|
|
1699
|
-
})
|
|
1700
|
-
|
|
1701
|
-
asset
|
|
1702
|
-
.command('track')
|
|
1703
|
-
.description('Log a usage event for an asset')
|
|
1704
|
-
.requiredOption('--id <uuid>', 'Asset ID')
|
|
1705
|
-
.requiredOption('-e, --event <event>', 'Event name (e.g. "renewed", "deployed", "rotated")')
|
|
1706
|
-
.option('--actor <name>', 'Who performed the action')
|
|
1707
|
-
.option('--meta <json>', 'Event metadata JSON')
|
|
1708
|
-
.action(async (opts) => {
|
|
1709
|
-
const metadata = opts.meta ? JSON.parse(opts.meta) : undefined
|
|
1710
|
-
const entry = await trackAssetUsage(opts.id, opts.event, opts.actor, metadata)
|
|
1711
|
-
success(`Tracked: ${colorize(opts.event, 'cyan')} on ${colorize(opts.id, 'dim')} at ${colorize(entry.created_at, 'dim')}`)
|
|
1712
|
-
})
|
|
1713
|
-
|
|
1714
|
-
asset
|
|
1715
|
-
.command('usage')
|
|
1716
|
-
.description('View usage log for an asset')
|
|
1717
|
-
.requiredOption('--id <uuid>', 'Asset ID')
|
|
1718
|
-
.option('--limit <n>', 'Max entries', '20')
|
|
1719
|
-
.action(async (opts) => {
|
|
1720
|
-
const events = await listAssetUsage(opts.id, parseInt(opts.limit))
|
|
1721
|
-
if (events.length === 0) {
|
|
1722
|
-
console.log('No usage events found.')
|
|
1723
|
-
return
|
|
1724
|
-
}
|
|
1725
|
-
for (const e of events) {
|
|
1726
|
-
console.log(`${e.created_at} | ${(e.actor ?? '-').padEnd(10)} | ${e.event} ${Object.keys(e.metadata).length > 0 ? JSON.stringify(e.metadata) : ''}`)
|
|
1727
|
-
}
|
|
1728
|
-
console.log(`\n${events.length} events`)
|
|
1729
|
-
})
|
|
1730
|
-
|
|
1731
1418
|
program.parseAsync()
|