optimal-cli 0.1.0 → 1.0.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.
Files changed (107) hide show
  1. package/agents/.gitkeep +0 -0
  2. package/agents/content-ops.md +227 -0
  3. package/agents/financial-ops.md +184 -0
  4. package/agents/infra-ops.md +206 -0
  5. package/agents/profiles.json +5 -0
  6. package/bin/optimal.ts +1731 -0
  7. package/docs/CLI-REFERENCE.md +361 -0
  8. package/lib/assets/index.ts +225 -0
  9. package/lib/assets.ts +124 -0
  10. package/lib/auth/index.ts +189 -0
  11. package/lib/board/index.ts +309 -0
  12. package/lib/board/types.ts +124 -0
  13. package/lib/bot/claim.ts +43 -0
  14. package/lib/bot/coordinator.ts +254 -0
  15. package/lib/bot/heartbeat.ts +37 -0
  16. package/lib/bot/index.ts +9 -0
  17. package/lib/bot/protocol.ts +99 -0
  18. package/lib/bot/reporter.ts +42 -0
  19. package/lib/bot/skills.ts +81 -0
  20. package/lib/budget/projections.ts +561 -0
  21. package/lib/budget/scenarios.ts +312 -0
  22. package/lib/cms/publish-blog.ts +129 -0
  23. package/lib/cms/strapi-client.ts +302 -0
  24. package/lib/config/registry.ts +228 -0
  25. package/lib/config/schema.ts +58 -0
  26. package/lib/config.ts +247 -0
  27. package/lib/errors.ts +129 -0
  28. package/lib/format.ts +120 -0
  29. package/lib/infra/.gitkeep +0 -0
  30. package/lib/infra/deploy.ts +70 -0
  31. package/lib/infra/migrate.ts +141 -0
  32. package/lib/newsletter/.gitkeep +0 -0
  33. package/lib/newsletter/distribute.ts +256 -0
  34. package/{dist/lib/newsletter/generate-insurance.d.ts → lib/newsletter/generate-insurance.ts} +24 -7
  35. package/lib/newsletter/generate.ts +735 -0
  36. package/lib/returnpro/.gitkeep +0 -0
  37. package/lib/returnpro/anomalies.ts +258 -0
  38. package/lib/returnpro/audit.ts +194 -0
  39. package/lib/returnpro/diagnose.ts +400 -0
  40. package/lib/returnpro/kpis.ts +255 -0
  41. package/lib/returnpro/templates.ts +323 -0
  42. package/lib/returnpro/upload-income.ts +311 -0
  43. package/lib/returnpro/upload-netsuite.ts +696 -0
  44. package/lib/returnpro/upload-r1.ts +563 -0
  45. package/lib/returnpro/validate.ts +154 -0
  46. package/lib/social/meta.ts +228 -0
  47. package/lib/social/post-generator.ts +468 -0
  48. package/lib/social/publish.ts +301 -0
  49. package/lib/social/scraper.ts +503 -0
  50. package/lib/supabase.ts +25 -0
  51. package/lib/transactions/delete-batch.ts +258 -0
  52. package/lib/transactions/ingest.ts +659 -0
  53. package/lib/transactions/stamp.ts +654 -0
  54. package/package.json +15 -25
  55. package/dist/bin/optimal.d.ts +0 -2
  56. package/dist/bin/optimal.js +0 -995
  57. package/dist/lib/budget/projections.d.ts +0 -115
  58. package/dist/lib/budget/projections.js +0 -384
  59. package/dist/lib/budget/scenarios.d.ts +0 -93
  60. package/dist/lib/budget/scenarios.js +0 -214
  61. package/dist/lib/cms/publish-blog.d.ts +0 -62
  62. package/dist/lib/cms/publish-blog.js +0 -74
  63. package/dist/lib/cms/strapi-client.d.ts +0 -123
  64. package/dist/lib/cms/strapi-client.js +0 -213
  65. package/dist/lib/config.d.ts +0 -55
  66. package/dist/lib/config.js +0 -206
  67. package/dist/lib/infra/deploy.d.ts +0 -29
  68. package/dist/lib/infra/deploy.js +0 -58
  69. package/dist/lib/infra/migrate.d.ts +0 -34
  70. package/dist/lib/infra/migrate.js +0 -103
  71. package/dist/lib/kanban.d.ts +0 -46
  72. package/dist/lib/kanban.js +0 -118
  73. package/dist/lib/newsletter/distribute.d.ts +0 -52
  74. package/dist/lib/newsletter/distribute.js +0 -193
  75. package/dist/lib/newsletter/generate-insurance.js +0 -36
  76. package/dist/lib/newsletter/generate.d.ts +0 -104
  77. package/dist/lib/newsletter/generate.js +0 -571
  78. package/dist/lib/returnpro/anomalies.d.ts +0 -64
  79. package/dist/lib/returnpro/anomalies.js +0 -166
  80. package/dist/lib/returnpro/audit.d.ts +0 -32
  81. package/dist/lib/returnpro/audit.js +0 -147
  82. package/dist/lib/returnpro/diagnose.d.ts +0 -52
  83. package/dist/lib/returnpro/diagnose.js +0 -281
  84. package/dist/lib/returnpro/kpis.d.ts +0 -32
  85. package/dist/lib/returnpro/kpis.js +0 -192
  86. package/dist/lib/returnpro/templates.d.ts +0 -48
  87. package/dist/lib/returnpro/templates.js +0 -229
  88. package/dist/lib/returnpro/upload-income.d.ts +0 -25
  89. package/dist/lib/returnpro/upload-income.js +0 -235
  90. package/dist/lib/returnpro/upload-netsuite.d.ts +0 -37
  91. package/dist/lib/returnpro/upload-netsuite.js +0 -566
  92. package/dist/lib/returnpro/upload-r1.d.ts +0 -48
  93. package/dist/lib/returnpro/upload-r1.js +0 -398
  94. package/dist/lib/social/post-generator.d.ts +0 -83
  95. package/dist/lib/social/post-generator.js +0 -333
  96. package/dist/lib/social/publish.d.ts +0 -66
  97. package/dist/lib/social/publish.js +0 -226
  98. package/dist/lib/social/scraper.d.ts +0 -67
  99. package/dist/lib/social/scraper.js +0 -361
  100. package/dist/lib/supabase.d.ts +0 -4
  101. package/dist/lib/supabase.js +0 -20
  102. package/dist/lib/transactions/delete-batch.d.ts +0 -60
  103. package/dist/lib/transactions/delete-batch.js +0 -203
  104. package/dist/lib/transactions/ingest.d.ts +0 -43
  105. package/dist/lib/transactions/ingest.js +0 -555
  106. package/dist/lib/transactions/stamp.d.ts +0 -51
  107. package/dist/lib/transactions/stamp.js +0 -524
@@ -0,0 +1,361 @@
1
+ # optimal-cli Command Reference
2
+
3
+ > 51 commands across 9 command groups + 12 standalone commands
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Run any command
9
+ npx tsx bin/optimal.ts <command> [options]
10
+
11
+ # Or alias it
12
+ alias optimal="npx tsx bin/optimal.ts"
13
+ ```
14
+
15
+ ## Environment Variables
16
+
17
+ ```bash
18
+ # Required for board/bot/project/asset commands (OptimalOS Supabase)
19
+ OPTIMAL_SUPABASE_URL=https://hbfalrpswysryltysonm.supabase.co
20
+ OPTIMAL_SUPABASE_SERVICE_KEY=<service_role_key>
21
+
22
+ # Required for financial commands (ReturnPro Supabase)
23
+ RETURNPRO_SUPABASE_URL=https://vvutttwunexshxkmygik.supabase.co
24
+ RETURNPRO_SUPABASE_SERVICE_KEY=<service_role_key>
25
+
26
+ # Required for content commands
27
+ STRAPI_URL=https://strapi.op-hub.com
28
+ STRAPI_API_TOKEN=<token>
29
+ GROQ_API_KEY=<key>
30
+ GROQ_MODEL=llama-3.3-70b-versatile
31
+
32
+ # Required for Instagram publishing
33
+ META_ACCESS_TOKEN=<meta_graph_api_token>
34
+ META_IG_ACCOUNT_ID_CRE_11TRUST=<ig_account_id>
35
+ META_IG_ACCOUNT_ID_LIFEINSUR=<ig_account_id>
36
+
37
+ # Optional
38
+ NEWSAPI_KEY=<key>
39
+ NO_COLOR=1 # Disable colored output
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Board Commands (Kanban)
45
+
46
+ Manage the task kanban board. All tasks live in Supabase.
47
+
48
+ ```bash
49
+ # View the full board
50
+ optimal board view
51
+
52
+ # Filter by status, project, or agent
53
+ optimal board view -s ready
54
+ optimal board view -s in_progress
55
+ optimal board view -p cli-consolidation
56
+ optimal board view --mine bot1
57
+
58
+ # Create a task
59
+ optimal board create \
60
+ -t "Fix login bug" \
61
+ -p cli-consolidation \
62
+ --priority 1 \
63
+ --effort m \
64
+ --skill "auth"
65
+
66
+ # Update a task
67
+ optimal board update --id <uuid> --status in_progress
68
+ optimal board update --id <uuid> --priority 2
69
+
70
+ # Claim a task (bot pull model)
71
+ optimal board claim --id <uuid> --agent bot1
72
+
73
+ # Comment on a task
74
+ optimal board comment --id <uuid> --author bot1 --body "Working on it"
75
+
76
+ # View activity log
77
+ optimal board log
78
+ optimal board log --actor bot1 --limit 10
79
+ ```
80
+
81
+ **Statuses:** `backlog` > `ready` > `claimed` > `in_progress` > `review` > `done` | `blocked`
82
+
83
+ **Priorities:** P1 (Critical), P2 (High), P3 (Medium), P4 (Low)
84
+
85
+ **Effort sizes:** xs, s, m, l, xl
86
+
87
+ ---
88
+
89
+ ## Bot Commands (Agent Orchestration)
90
+
91
+ Manage bot agents that claim and work tasks from the board.
92
+
93
+ ```bash
94
+ # Send heartbeat (proves agent is alive)
95
+ optimal bot heartbeat --agent bot1
96
+ optimal bot heartbeat --agent bot1 --status working
97
+
98
+ # List active agents (heartbeat in last 5 min)
99
+ optimal bot agents
100
+
101
+ # Claim the next available task (auto-selects highest priority)
102
+ optimal bot claim --agent bot1
103
+ optimal bot claim --agent bot1 --skill generate-social-posts
104
+
105
+ # Report progress
106
+ optimal bot report --task <uuid> --agent bot1 --message "50% complete"
107
+
108
+ # Mark task done
109
+ optimal bot complete --task <uuid> --agent bot1 --summary "All tests pass"
110
+
111
+ # Release a task back to ready
112
+ optimal bot release --task <uuid> --agent bot1
113
+ optimal bot release --task <uuid> --agent bot1 --reason "Need more context"
114
+
115
+ # Mark task blocked
116
+ optimal bot blocked --task <uuid> --agent bot1 --reason "Waiting on API key"
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Project Commands
122
+
123
+ Manage project groupings that tasks belong to.
124
+
125
+ ```bash
126
+ optimal project list
127
+
128
+ optimal project create \
129
+ --slug my-project \
130
+ --name "My Project" \
131
+ --priority 1 \
132
+ --owner clenisa
133
+
134
+ optimal project update --slug my-project -s completed
135
+ optimal project update --slug my-project --priority 2
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Asset Commands (Digital Asset Tracking)
141
+
142
+ Track domains, servers, API keys, services, and repos.
143
+
144
+ ```bash
145
+ # List all assets
146
+ optimal asset list
147
+ optimal asset list --type domain --status active
148
+
149
+ # Add an asset
150
+ optimal asset add \
151
+ --name "op-hub.com" \
152
+ --type domain \
153
+ --owner clenisa \
154
+ --expires "2027-01-15"
155
+
156
+ # Update asset
157
+ optimal asset update --id <uuid> --status inactive
158
+
159
+ # Get single asset
160
+ optimal asset get --id <uuid>
161
+
162
+ # Remove asset
163
+ optimal asset remove --id <uuid>
164
+
165
+ # Track usage event
166
+ optimal asset track --id <uuid> --event "SSL renewed" --actor clenisa
167
+
168
+ # View usage log
169
+ optimal asset usage --id <uuid>
170
+ ```
171
+
172
+ **Asset types:** domain, server, api_key, service, repo
173
+
174
+ ---
175
+
176
+ ## Financial Commands (ReturnPro)
177
+
178
+ All financial data flows through the ReturnPro Supabase instance.
179
+
180
+ ```bash
181
+ # Upload financial data
182
+ optimal upload-netsuite --file data.csv --period 2025-06
183
+ optimal upload-r1 --file r1-export.xlsx --period 2025-06
184
+ optimal upload-income-statements --file confirmed.csv
185
+
186
+ # Audit & diagnostics
187
+ optimal audit-financials --months 2025-01,2025-02
188
+ optimal diagnose-months --months 2025-03
189
+ optimal rate-anomalies --client "Acme Corp" --period 2025-06
190
+
191
+ # KPIs
192
+ optimal export-kpis --format table
193
+ optimal export-kpis --format csv > kpis.csv
194
+
195
+ # Budget projections
196
+ optimal project-budget --adjustment-type percentage --adjustment-value 4
197
+ optimal export-budget --format csv > budget.csv
198
+
199
+ # Batch delete (dry-run by default)
200
+ optimal delete-batch --table stg_financials_raw --client "Test Corp"
201
+ optimal delete-batch --table stg_financials_raw --client "Test Corp" --execute
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Budget Scenario Commands
207
+
208
+ Save, compare, and manage budget projection scenarios.
209
+
210
+ ```bash
211
+ optimal scenario list
212
+
213
+ optimal scenario save \
214
+ --name "4pct-growth" \
215
+ --adjustment-type percentage \
216
+ --adjustment-value 4
217
+
218
+ optimal scenario compare --names "baseline,4pct-growth"
219
+
220
+ optimal scenario delete --name "old-scenario"
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Content Commands (Newsletter, Social, Blog)
226
+
227
+ Generate and publish content across channels.
228
+
229
+ ```bash
230
+ # Generate newsletter (AI-powered via Groq)
231
+ optimal generate-newsletter --brand CRE-11TRUST
232
+ optimal generate-newsletter --brand LIFEINSUR
233
+
234
+ # Distribute newsletter
235
+ optimal distribute-newsletter --newsletter-id 42
236
+ optimal distribution-status --newsletter-id 42
237
+
238
+ # Generate social posts (AI-powered)
239
+ optimal generate-social-posts --brand CRE-11TRUST --count 5
240
+
241
+ # View social queue
242
+ optimal social-queue --brand CRE-11TRUST
243
+
244
+ # Publish social posts (via n8n)
245
+ optimal publish-social-posts --brand CRE-11TRUST
246
+
247
+ # Publish to Instagram (direct Meta Graph API)
248
+ optimal publish-instagram --brand CRE-11TRUST
249
+ optimal publish-instagram --brand CRE-11TRUST --dry-run
250
+ optimal publish-instagram --brand LIFEINSUR --limit 3
251
+
252
+ # Scrape competitor ads
253
+ optimal scrape-ads --brand CRE-11TRUST
254
+
255
+ # Blog
256
+ optimal blog-drafts --brand CRE-11TRUST
257
+ optimal publish-blog --id 15
258
+ ```
259
+
260
+ **Brands:** `CRE-11TRUST` (ElevenTrust, commercial real estate), `LIFEINSUR` (AnchorPoint, insurance)
261
+
262
+ ---
263
+
264
+ ## Config Commands
265
+
266
+ Manage local CLI configuration.
267
+
268
+ ```bash
269
+ optimal config init --owner oracle --brand CRE-11TRUST
270
+ optimal config doctor # Validate config
271
+ optimal config export --out ./backup.json
272
+ optimal config import --in ./backup.json
273
+ optimal config sync # Sync with shared registry
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Infrastructure Commands
279
+
280
+ ```bash
281
+ # Deploy apps to Vercel
282
+ optimal deploy dashboard --prod
283
+ optimal deploy board # Preview deployment
284
+
285
+ # Database migrations
286
+ optimal migrate pending --target optimalos
287
+ optimal migrate push --target returnpro --dry-run
288
+ optimal migrate push --target optimalos
289
+ optimal migrate create --target optimalos --name "add-index"
290
+
291
+ # Health check
292
+ optimal health-check
293
+
294
+ # Generate upload templates
295
+ optimal generate-netsuite-template --output template.xlsx
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Milestone & Label Commands
301
+
302
+ ```bash
303
+ # Milestones
304
+ optimal milestone create --project <uuid> --name "v1.0" --due 2026-04-01
305
+ optimal milestone list
306
+ optimal milestone list --project <uuid>
307
+
308
+ # Labels
309
+ optimal label create --name "migration" --color "#3B82F6"
310
+ optimal label list
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Architecture Overview
316
+
317
+ ```
318
+ optimal-cli/
319
+ bin/optimal.ts CLI entry point (Commander.js)
320
+ lib/ Implementation modules
321
+ auth/ Auth primitives (session, service client)
322
+ assets/ Asset tracking CRUD
323
+ board/ Kanban board CRUD + formatting
324
+ bot/ Bot orchestration (heartbeat, claim, report, skills, coordinator)
325
+ budget/ Budget projections + scenarios
326
+ cms/ Strapi client + blog publishing
327
+ config/ Config registry + schema
328
+ format.ts ANSI colors, tables, badges
329
+ infra/ Deploy + migrate
330
+ newsletter/ Newsletter generation + distribution
331
+ returnpro/ Financial uploads, audit, KPIs, anomalies, validation
332
+ social/ Social post generation, publishing, scraping, Meta API
333
+ supabase.ts Supabase client factory (dual-instance)
334
+ transactions/ Transaction ingestion, stamping, batch delete
335
+ apps/ Read-only Next.js dashboards
336
+ board/ Kanban board (deployed: optimal-board.vercel.app)
337
+ newsletter-preview/ Newsletter HTML preview
338
+ returnpro-dashboard/ ReturnPro financial overview
339
+ wes-dashboard/ Budget dashboard
340
+ activity/ Agent activity timeline
341
+ portfolio/ Portfolio site stub
342
+ agents/profiles.json Bot agent definitions
343
+ scripts/ Seed scripts
344
+ skills/ Agent-facing skill definitions (.md)
345
+ supabase/migrations/ SQL migration files
346
+ tests/ Test suite (node:test)
347
+ docs/ Plans, specs, API docs
348
+ ```
349
+
350
+ ## Supabase Instances
351
+
352
+ | Instance | Ref | Tables | Used By |
353
+ |----------|-----|--------|---------|
354
+ | OptimalOS | hbfalrpswysryltysonm | projects, tasks, milestones, labels, comments, activity_log, task_labels | board, bot, project, asset commands |
355
+ | ReturnPro | vvutttwunexshxkmygik | stg_financials_raw, confirmed_income_statements, dim_account, dim_client, dim_master_program, dim_program_id | financial, budget, KPI commands |
356
+
357
+ ## Dashboard URLs
358
+
359
+ | Dashboard | URL | What It Shows |
360
+ |-----------|-----|---------------|
361
+ | Kanban Board | https://optimal-board.vercel.app | Task board, project progress, activity feed |
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Asset tracking — manage digital infrastructure items
3
+ * (domains, servers, API keys, services, repos).
4
+ * Stores in the OptimalOS Supabase instance.
5
+ */
6
+
7
+ import { getSupabase } from '../supabase.js'
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────
10
+
11
+ export type AssetType = 'domain' | 'server' | 'api_key' | 'service' | 'repo' | 'other'
12
+ export type AssetStatus = 'active' | 'inactive' | 'expired' | 'pending'
13
+
14
+ export interface Asset {
15
+ id: string
16
+ name: string
17
+ type: AssetType
18
+ status: AssetStatus
19
+ metadata: Record<string, unknown>
20
+ owner: string | null
21
+ expires_at: string | null
22
+ created_at: string
23
+ updated_at: string
24
+ }
25
+
26
+ export interface CreateAssetInput {
27
+ name: string
28
+ type: AssetType
29
+ status?: AssetStatus
30
+ metadata?: Record<string, unknown>
31
+ owner?: string
32
+ expires_at?: string
33
+ }
34
+
35
+ export interface UpdateAssetInput {
36
+ name?: string
37
+ type?: AssetType
38
+ status?: AssetStatus
39
+ metadata?: Record<string, unknown>
40
+ owner?: string
41
+ expires_at?: string | null
42
+ }
43
+
44
+ export interface AssetFilters {
45
+ type?: AssetType
46
+ status?: AssetStatus
47
+ owner?: string
48
+ }
49
+
50
+ export interface AssetUsageEvent {
51
+ id: string
52
+ asset_id: string
53
+ event: string
54
+ actor: string | null
55
+ metadata: Record<string, unknown>
56
+ created_at: string
57
+ }
58
+
59
+ // ── Supabase accessor ────────────────────────────────────────────────
60
+
61
+ const sb = () => getSupabase('optimal')
62
+
63
+ // ── CRUD operations ──────────────────────────────────────────────────
64
+
65
+ /**
66
+ * List assets, optionally filtered by type, status, or owner.
67
+ */
68
+ export async function listAssets(filters?: AssetFilters): Promise<Asset[]> {
69
+ let query = sb().from('assets').select('*')
70
+
71
+ if (filters?.type) query = query.eq('type', filters.type)
72
+ if (filters?.status) query = query.eq('status', filters.status)
73
+ if (filters?.owner) query = query.eq('owner', filters.owner)
74
+
75
+ const { data, error } = await query.order('updated_at', { ascending: false })
76
+ if (error) throw new Error(`Failed to list assets: ${error.message}`)
77
+ return (data ?? []) as Asset[]
78
+ }
79
+
80
+ /**
81
+ * Create a new asset.
82
+ */
83
+ export async function createAsset(input: CreateAssetInput): Promise<Asset> {
84
+ const { data, error } = await sb()
85
+ .from('assets')
86
+ .insert({
87
+ name: input.name,
88
+ type: input.type,
89
+ status: input.status ?? 'active',
90
+ metadata: input.metadata ?? {},
91
+ owner: input.owner ?? null,
92
+ expires_at: input.expires_at ?? null,
93
+ })
94
+ .select()
95
+ .single()
96
+
97
+ if (error) throw new Error(`Failed to create asset: ${error.message}`)
98
+ return data as Asset
99
+ }
100
+
101
+ /**
102
+ * Update an existing asset by ID.
103
+ */
104
+ export async function updateAsset(id: string, updates: UpdateAssetInput): Promise<Asset> {
105
+ const { data, error } = await sb()
106
+ .from('assets')
107
+ .update(updates)
108
+ .eq('id', id)
109
+ .select()
110
+ .single()
111
+
112
+ if (error) throw new Error(`Failed to update asset: ${error.message}`)
113
+ return data as Asset
114
+ }
115
+
116
+ /**
117
+ * Get a single asset by ID.
118
+ */
119
+ export async function getAsset(id: string): Promise<Asset> {
120
+ const { data, error } = await sb()
121
+ .from('assets')
122
+ .select('*')
123
+ .eq('id', id)
124
+ .single()
125
+
126
+ if (error) throw new Error(`Asset not found: ${error.message}`)
127
+ return data as Asset
128
+ }
129
+
130
+ /**
131
+ * Delete an asset by ID.
132
+ */
133
+ export async function deleteAsset(id: string): Promise<void> {
134
+ const { error } = await sb()
135
+ .from('assets')
136
+ .delete()
137
+ .eq('id', id)
138
+
139
+ if (error) throw new Error(`Failed to delete asset: ${error.message}`)
140
+ }
141
+
142
+ // ── Usage tracking ───────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Log a usage event against an asset.
146
+ */
147
+ export async function trackAssetUsage(
148
+ assetId: string,
149
+ event: string,
150
+ actor?: string,
151
+ metadata?: Record<string, unknown>,
152
+ ): Promise<AssetUsageEvent> {
153
+ const { data, error } = await sb()
154
+ .from('asset_usage_log')
155
+ .insert({
156
+ asset_id: assetId,
157
+ event,
158
+ actor: actor ?? null,
159
+ metadata: metadata ?? {},
160
+ })
161
+ .select()
162
+ .single()
163
+
164
+ if (error) throw new Error(`Failed to track usage: ${error.message}`)
165
+ return data as AssetUsageEvent
166
+ }
167
+
168
+ /**
169
+ * List usage events for a given asset.
170
+ */
171
+ export async function listAssetUsage(assetId: string, limit = 50): Promise<AssetUsageEvent[]> {
172
+ const { data, error } = await sb()
173
+ .from('asset_usage_log')
174
+ .select('*')
175
+ .eq('asset_id', assetId)
176
+ .order('created_at', { ascending: false })
177
+ .limit(limit)
178
+
179
+ if (error) throw new Error(`Failed to list usage: ${error.message}`)
180
+ return (data ?? []) as AssetUsageEvent[]
181
+ }
182
+
183
+ // ── Formatting ───────────────────────────────────────────────────────
184
+
185
+ const TYPE_LABELS: Record<AssetType, string> = {
186
+ domain: 'Domain',
187
+ server: 'Server',
188
+ api_key: 'API Key',
189
+ service: 'Service',
190
+ repo: 'Repo',
191
+ other: 'Other',
192
+ }
193
+
194
+ /**
195
+ * Format assets into a table string for CLI display.
196
+ */
197
+ export function formatAssetTable(assets: Asset[]): string {
198
+ if (assets.length === 0) return 'No assets found.'
199
+
200
+ const headers = ['Type', 'Status', 'Name', 'Owner', 'Expires']
201
+ const rows = assets.map(a => [
202
+ TYPE_LABELS[a.type] ?? a.type,
203
+ a.status,
204
+ a.name.length > 35 ? a.name.slice(0, 32) + '...' : a.name,
205
+ a.owner ?? '-',
206
+ a.expires_at ? a.expires_at.slice(0, 10) : '-',
207
+ ])
208
+
209
+ // Compute column widths
210
+ const widths = headers.map((h, i) => {
211
+ let max = h.length
212
+ for (const row of rows) {
213
+ if ((row[i]?.length ?? 0) > max) max = row[i].length
214
+ }
215
+ return max
216
+ })
217
+
218
+ const sep = '+-' + widths.map(w => '-'.repeat(w)).join('-+-') + '-+'
219
+ const headerRow = '| ' + headers.map((h, i) => h.padEnd(widths[i])).join(' | ') + ' |'
220
+ const bodyRows = rows.map(row =>
221
+ '| ' + row.map((cell, i) => (cell ?? '').padEnd(widths[i])).join(' | ') + ' |'
222
+ )
223
+
224
+ return [sep, headerRow, sep, ...bodyRows, sep, `\nTotal: ${assets.length} assets`].join('\n')
225
+ }
package/lib/assets.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { createClient, SupabaseClient } from '@supabase/supabase-js'
2
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
3
+ import { join, basename } from 'node:path'
4
+ import { homedir } from 'node:os'
5
+ import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto'
6
+
7
+ const CONFIG_DIR = join(homedir(), '.openclaw')
8
+ const SKILLS_DIR = join(CONFIG_DIR, 'skills')
9
+ const PLUGINS_DIR = join(CONFIG_DIR, 'plugins')
10
+ const WORKSPACE_DIR = join(homedir(), '.openclaw', 'workspace')
11
+
12
+ const ALGORITHM = 'aes-256-gcm'
13
+
14
+ function getSupabase(): SupabaseClient {
15
+ const url = process.env.OPTIMAL_SUPABASE_URL
16
+ const key = process.env.OPTIMAL_SUPABASE_SERVICE_KEY
17
+ if (!url || !key) throw new Error('OPTIMAL_SUPABASE_URL and OPTIMAL_SUPABASE_SERVICE_KEY required')
18
+ return createClient(url, key)
19
+ }
20
+
21
+ function hashContent(content: string): string {
22
+ return createHash('sha256').update(content).digest('hex')
23
+ }
24
+
25
+ export interface ScannedAsset {
26
+ type: 'skill' | 'cli' | 'cron' | 'repo' | 'env' | 'ssh_key' | 'plugin'
27
+ name: string
28
+ version?: string
29
+ path: string
30
+ hash: string
31
+ content?: string
32
+ metadata: Record<string, unknown>
33
+ }
34
+
35
+ export function scanSkills(): ScannedAsset[] {
36
+ const assets: ScannedAsset[] = []
37
+ if (!existsSync(SKILLS_DIR)) return assets
38
+ for (const dir of readdirSync(SKILLS_DIR)) {
39
+ const skillPath = join(SKILLS_DIR, dir)
40
+ if (!statSync(skillPath).isDirectory()) continue
41
+ const skillFile = join(skillPath, 'SKILL.md')
42
+ if (existsSync(skillFile)) {
43
+ const content = readFileSync(skillFile, 'utf-8')
44
+ assets.push({ type: 'skill', name: dir, path: skillFile, hash: hashContent(content), content, metadata: {} })
45
+ }
46
+ }
47
+ return assets
48
+ }
49
+
50
+ export function scanPlugins(): ScannedAsset[] {
51
+ const assets: ScannedAsset[] = []
52
+ if (!existsSync(PLUGINS_DIR)) return assets
53
+ for (const dir of readdirSync(PLUGINS_DIR)) {
54
+ const pluginPath = join(PLUGINS_DIR, dir)
55
+ if (!statSync(pluginPath).isDirectory()) continue
56
+ assets.push({ type: 'plugin', name: dir, path: pluginPath, hash: hashContent(dir), metadata: {} })
57
+ }
58
+ return assets
59
+ }
60
+
61
+ export function scanCLIs(): ScannedAsset[] {
62
+ const assets: ScannedAsset[] = []
63
+ const knownCLIs = ['vercel', 'supabase', 'gh', 'openclaw']
64
+ for (const cli of knownCLIs) {
65
+ try {
66
+ const { execSync } = require('node:child_process')
67
+ const version = execSync(`${cli} --version 2>/dev/null || echo ""`).toString().trim()
68
+ if (version) {
69
+ assets.push({ type: 'cli', name: cli, version: version.slice(0, 20), path: '', hash: hashContent(version), metadata: {} })
70
+ }
71
+ } catch {}
72
+ }
73
+ return assets
74
+ }
75
+
76
+ export function scanRepos(): ScannedAsset[] {
77
+ const assets: ScannedAsset[] = []
78
+ if (!existsSync(WORKSPACE_DIR)) return assets
79
+ for (const dir of readdirSync(WORKSPACE_DIR)) {
80
+ const repoPath = join(WORKSPACE_DIR, dir)
81
+ if (!statSync(repoPath).isDirectory()) continue
82
+ if (existsSync(join(repoPath, '.git'))) {
83
+ assets.push({ type: 'repo', name: dir, path: repoPath, hash: '', metadata: {} })
84
+ }
85
+ }
86
+ return assets
87
+ }
88
+
89
+ export function scanAllAssets(): ScannedAsset[] {
90
+ return [...scanSkills(), ...scanPlugins(), ...scanCLIs(), ...scanRepos()]
91
+ }
92
+
93
+ export async function pushAssets(agentName: string): Promise<{ pushed: number; updated: number }> {
94
+ const supabase = getSupabase()
95
+ const assets = scanAllAssets()
96
+ let pushed = 0, updated = 0
97
+ for (const asset of assets) {
98
+ const { data: existing } = await supabase.from('agent_assets').select('id, asset_hash').eq('agent_name', agentName).eq('asset_type', asset.type).eq('asset_name', asset.name).single()
99
+ if (existing) {
100
+ if (existing.asset_hash !== asset.hash) {
101
+ await supabase.from('agent_assets').update({ asset_version: asset.version, asset_path: asset.path, asset_hash: asset.hash, content: asset.content, metadata: asset.metadata, updated_at: new Date().toISOString() }).eq('id', existing.id)
102
+ updated++
103
+ }
104
+ } else {
105
+ await supabase.from('agent_assets').insert({ agent_name: agentName, asset_type: asset.type, asset_name: asset.name, asset_version: asset.version, asset_path: asset.path, asset_hash: asset.hash, content: asset.content, metadata: asset.metadata })
106
+ pushed++
107
+ }
108
+ }
109
+ return { pushed, updated }
110
+ }
111
+
112
+ export async function listAssets(agentName?: string): Promise<any[]> {
113
+ const supabase = getSupabase()
114
+ let query = supabase.from('agent_assets').select('*')
115
+ if (agentName) query = query.eq('agent_name', agentName)
116
+ const { data } = await query.order('updated_at', { ascending: false })
117
+ return data || []
118
+ }
119
+
120
+ export async function getInventory(): Promise<any[]> {
121
+ const supabase = getSupabase()
122
+ const { data } = await supabase.from('agent_inventory').select('*')
123
+ return data || []
124
+ }