moltengine-cli 0.1.4 → 0.1.6

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/moltengine.js +539 -141
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltengine-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for Moltengine - The WP Engine for AI Agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/moltengine.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { request } from "undici";
4
+ import { exec } from "child_process";
5
+ import { createInterface } from "readline";
4
6
 
5
7
  // Configuration
6
- const API_URL = process.env.MOLTENGINE_API || "http://localhost:8080";
8
+ const API_URL = process.env.MOLTENGINE_API || "https://api.moltengine.com";
7
9
  const TENANT_KEY = process.env.MOLT_TENANT_KEY || "";
8
10
  const WEB_URL = process.env.MOLTENGINE_WEB || "https://moltengine.com";
9
11
 
@@ -60,9 +62,11 @@ ${colors.primary} __ __ _ _ _
60
62
  `;
61
63
 
62
64
  const ASCII_LOGO_COMPACT = `
63
- ${colors.primary} ╔╦╗╔═╗╦ ╔╦╗╔═╗╔╗╔╔═╗╦╔╗╔╔═╗
64
- ║║║║ ║║ ║ ║╣ ║║║║ ╦║║║║║╣
65
- ╩╚═╝╩═╝╩ ╚═╝╝╚╝╚═╝╩╝╚╝╚═╝${colors.reset}
65
+ ${colors.primary} __ __ _ _ _
66
+ | \\/ |___| | |_ ___ _ _ _(_)_ _ ___
67
+ | |\\/| / _ \\ | __/ -_) ' \\/ _\` | ' \\/ -_)
68
+ |_| |_\\___/_|\\__\\___|_||_\\__, |_||_\\___|
69
+ |___/${colors.reset}
66
70
  `;
67
71
 
68
72
  // ============================================================================
@@ -93,6 +97,48 @@ function error(message) {
93
97
  process.exit(1);
94
98
  }
95
99
 
100
+ /**
101
+ * Check if tenant needs to complete payment and prompt if so
102
+ * Returns true if payment is needed (and command should stop)
103
+ */
104
+ function checkPaymentRequired(tenant) {
105
+ const unpaidStatuses = ['pending', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid', 'trialing'];
106
+ const billingStatus = tenant.billingStatus || tenant.billing_status || tenant.status;
107
+
108
+ // Check if tenant needs to pay
109
+ if (unpaidStatuses.includes(billingStatus?.toLowerCase())) {
110
+ console.log(ASCII_LOGO_COMPACT);
111
+ console.log();
112
+ print(`${colors.warning}⚡ Subscription Required${colors.reset}`, colors.bold);
113
+ console.log();
114
+
115
+ if (billingStatus === 'past_due') {
116
+ console.log(` Your subscription payment is past due.`);
117
+ console.log(` Please update your payment method to continue.`);
118
+ } else {
119
+ console.log(` Complete your subscription to start using Moltengine.`);
120
+ }
121
+
122
+ console.log();
123
+ print("Plans", colors.bold);
124
+ print("─".repeat(40), colors.muted);
125
+ console.log(` ${colors.accent}Starter${colors.reset} ${PLAN_PRICES.starter} — 1 agent, all tools, approvals`);
126
+ console.log(` ${colors.accent}Team${colors.reset} ${PLAN_PRICES.team} — 5 agents, approvals`);
127
+ console.log(` ${colors.accent}Business${colors.reset} ${PLAN_PRICES.business} — 25 agents, SIEM, RBAC`);
128
+ console.log(` ${colors.accent}Enterprise${colors.reset} ${PLAN_PRICES.enterprise} — SSO, VPC, compliance`);
129
+ console.log();
130
+
131
+ const checkoutUrl = `${WEB_URL}/checkout`;
132
+ print(`${colors.info}→ Subscribe now: ${colors.accent}${checkoutUrl}${colors.reset}`, colors.bold);
133
+ console.log();
134
+
135
+ return true;
136
+ }
137
+
138
+ return false;
139
+ }
140
+
141
+
96
142
  /**
97
143
  * Print success message
98
144
  */
@@ -111,12 +157,42 @@ function info(message) {
111
157
  // API CLIENT
112
158
  // ============================================================================
113
159
 
160
+ /**
161
+ * Show signup/login prompt when no tenant key is set
162
+ */
163
+ function showAuthRequired() {
164
+ console.log(ASCII_LOGO_COMPACT);
165
+ console.log();
166
+ print(`${colors.warning}🔑 Authentication Required${colors.reset}`, colors.bold);
167
+ console.log();
168
+ console.log(` You need a Moltengine account to use the CLI.`);
169
+ console.log();
170
+ print("Get Started", colors.bold);
171
+ print("─".repeat(40), colors.muted);
172
+ console.log(` 1. Sign up or log in at ${colors.accent}${WEB_URL}/login${colors.reset}`);
173
+ console.log(` 2. Subscribe to a plan at ${colors.accent}${WEB_URL}/checkout${colors.reset}`);
174
+ console.log(` 3. Copy your tenant key from the dashboard`);
175
+ console.log(` 4. Set it in your environment:`);
176
+ console.log();
177
+ console.log(` ${colors.muted}export MOLT_TENANT_KEY="your-key-here"${colors.reset}`);
178
+ console.log();
179
+ print("Plans", colors.bold);
180
+ print("─".repeat(40), colors.muted);
181
+ console.log(` ${colors.accent}Starter${colors.reset} ${PLAN_PRICES.starter} — 1 agent, all tools, approvals`);
182
+ console.log(` ${colors.accent}Team${colors.reset} ${PLAN_PRICES.team} — 5 agents, approvals`);
183
+ console.log(` ${colors.accent}Business${colors.reset} ${PLAN_PRICES.business} — 25 agents, SIEM, RBAC`);
184
+ console.log();
185
+ print(`${colors.info}→ Start free: ${colors.accent}${WEB_URL}/login${colors.reset}`, colors.bold);
186
+ console.log();
187
+ process.exit(0);
188
+ }
189
+
114
190
  /**
115
191
  * Make an authenticated GET request
116
192
  */
117
193
  async function apiGet(path) {
118
194
  if (!TENANT_KEY) {
119
- error("MOLT_TENANT_KEY environment variable is required");
195
+ showAuthRequired();
120
196
  }
121
197
 
122
198
  const url = `${API_URL}${path}`;
@@ -151,7 +227,7 @@ async function apiGet(path) {
151
227
  */
152
228
  async function apiPost(path, body = {}) {
153
229
  if (!TENANT_KEY) {
154
- error("MOLT_TENANT_KEY environment variable is required");
230
+ showAuthRequired();
155
231
  }
156
232
 
157
233
  const url = `${API_URL}${path}`;
@@ -266,10 +342,316 @@ function padLeft(str, len) {
266
342
  return str.padStart(len);
267
343
  }
268
344
 
345
+ // ============================================================================
346
+ // BROWSER HELPER
347
+ // ============================================================================
348
+
349
+ /**
350
+ * Open a URL in the default browser
351
+ */
352
+ function openBrowser(url) {
353
+ const platform = process.platform;
354
+ let command;
355
+
356
+ if (platform === "darwin") {
357
+ command = `open "${url}"`;
358
+ } else if (platform === "win32") {
359
+ command = `start "" "${url}"`;
360
+ } else {
361
+ // Linux and others
362
+ command = `xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || x-www-browser "${url}" 2>/dev/null || gnome-open "${url}" 2>/dev/null`;
363
+ }
364
+
365
+ return new Promise((resolve) => {
366
+ exec(command, (err) => {
367
+ resolve(!err);
368
+ });
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Prompt user for input
374
+ */
375
+ function prompt(question) {
376
+ const rl = createInterface({
377
+ input: process.stdin,
378
+ output: process.stdout
379
+ });
380
+
381
+ return new Promise((resolve) => {
382
+ rl.question(question, (answer) => {
383
+ rl.close();
384
+ resolve(answer.trim());
385
+ });
386
+ });
387
+ }
388
+
269
389
  // ============================================================================
270
390
  // COMMANDS
271
391
  // ============================================================================
272
392
 
393
+ /**
394
+ * Login command - authenticate and get API key
395
+ */
396
+ async function cmdLogin() {
397
+ console.log(ASCII_LOGO_COMPACT);
398
+
399
+ // Check if already authenticated
400
+ if (TENANT_KEY) {
401
+ try {
402
+ const tenant = await apiGet("/tenant/me");
403
+ const billingStatus = tenant.billingStatus || tenant.billing_status || tenant.status;
404
+
405
+ if (billingStatus === "active") {
406
+ success("You're already logged in!");
407
+ console.log();
408
+ console.log(` ${colors.muted}Tenant:${colors.reset} ${tenant.name}`);
409
+ console.log(` ${colors.muted}Email:${colors.reset} ${tenant.email}`);
410
+ console.log(` ${colors.muted}Plan:${colors.reset} ${formatPlan(tenant.plan || "starter")}`);
411
+ console.log();
412
+ console.log(` ${colors.info}Run ${colors.accent}moltctl status${colors.reset} to see your dashboard.`);
413
+ console.log();
414
+ return;
415
+ }
416
+
417
+ // Has key but not paid - redirect to checkout
418
+ console.log();
419
+ print(`${colors.warning}⚡ Subscription Required${colors.reset}`, colors.bold);
420
+ console.log();
421
+ console.log(` Your account exists but needs an active subscription.`);
422
+ console.log();
423
+
424
+ const checkoutUrl = `${WEB_URL}/checkout`;
425
+ print(`Opening checkout in your browser...`, colors.info);
426
+ console.log();
427
+
428
+ const opened = await openBrowser(checkoutUrl);
429
+ if (!opened) {
430
+ console.log(` ${colors.muted}Could not open browser. Please visit:${colors.reset}`);
431
+ console.log(` ${colors.accent}${checkoutUrl}${colors.reset}`);
432
+ }
433
+
434
+ console.log();
435
+ console.log(` ${colors.muted}After subscribing, run ${colors.accent}moltctl status${colors.reset}${colors.muted} to verify.${colors.reset}`);
436
+ console.log();
437
+ return;
438
+ } catch {
439
+ // Key is invalid, proceed with login flow
440
+ }
441
+ }
442
+
443
+ // No key or invalid key - start login flow
444
+ console.log();
445
+ print("Welcome to Moltengine!", colors.bold);
446
+ console.log();
447
+ console.log(` Let's get you set up with a secure Moltbot runtime.`);
448
+ console.log();
449
+
450
+ print("Choose an option:", colors.bold);
451
+ print("─".repeat(40), colors.muted);
452
+ console.log(` ${colors.accent}[1]${colors.reset} Sign up / Subscribe (new users)`);
453
+ console.log(` ${colors.accent}[2]${colors.reset} Log in (existing users)`);
454
+ console.log(` ${colors.accent}[3]${colors.reset} Enter API key manually`);
455
+ console.log();
456
+
457
+ const choice = await prompt(` ${colors.muted}Enter choice (1/2/3):${colors.reset} `);
458
+ console.log();
459
+
460
+ if (choice === "1") {
461
+ // New user - go to checkout
462
+ const checkoutUrl = `${WEB_URL}/checkout`;
463
+ print("Opening checkout in your browser...", colors.info);
464
+
465
+ const opened = await openBrowser(checkoutUrl);
466
+ if (!opened) {
467
+ console.log();
468
+ console.log(` ${colors.muted}Could not open browser. Please visit:${colors.reset}`);
469
+ console.log(` ${colors.accent}${checkoutUrl}${colors.reset}`);
470
+ }
471
+
472
+ console.log();
473
+ print("After checkout:", colors.bold);
474
+ print("─".repeat(40), colors.muted);
475
+ console.log(` 1. Complete payment via Stripe`);
476
+ console.log(` 2. Copy your API key from the success page`);
477
+ console.log(` 3. Run: ${colors.accent}moltctl login${colors.reset} and choose option 3`);
478
+ console.log(` Or set: ${colors.accent}export MOLT_TENANT_KEY="your-key"${colors.reset}`);
479
+ console.log();
480
+
481
+ } else if (choice === "2") {
482
+ // Existing user - go to login
483
+ const loginUrl = `${WEB_URL}/login`;
484
+ print("Opening login page in your browser...", colors.info);
485
+
486
+ const opened = await openBrowser(loginUrl);
487
+ if (!opened) {
488
+ console.log();
489
+ console.log(` ${colors.muted}Could not open browser. Please visit:${colors.reset}`);
490
+ console.log(` ${colors.accent}${loginUrl}${colors.reset}`);
491
+ }
492
+
493
+ console.log();
494
+ print("After login:", colors.bold);
495
+ print("─".repeat(40), colors.muted);
496
+ console.log(` 1. Check your email for the magic link`);
497
+ console.log(` 2. Go to Credentials in your dashboard`);
498
+ console.log(` 3. Copy your API key`);
499
+ console.log(` 4. Run: ${colors.accent}moltctl login${colors.reset} and choose option 3`);
500
+ console.log(` Or set: ${colors.accent}export MOLT_TENANT_KEY="your-key"${colors.reset}`);
501
+ console.log();
502
+
503
+ } else if (choice === "3") {
504
+ // Manual key entry
505
+ const key = await prompt(` ${colors.muted}Enter your API key:${colors.reset} `);
506
+
507
+ if (!key) {
508
+ print("No key entered.", colors.warning);
509
+ console.log();
510
+ return;
511
+ }
512
+
513
+ // Validate the key
514
+ print("Validating key...", colors.info);
515
+
516
+ try {
517
+ // Temporarily set the key to validate
518
+ const response = await request(`${API_URL}/tenant/me`, {
519
+ method: "GET",
520
+ headers: {
521
+ "x-tenant-key": key,
522
+ "Accept": "application/json"
523
+ }
524
+ });
525
+
526
+ const text = await response.body.text();
527
+
528
+ if (response.statusCode >= 400) {
529
+ print("Invalid API key. Please check and try again.", colors.error);
530
+ console.log();
531
+ return;
532
+ }
533
+
534
+ const tenant = JSON.parse(text);
535
+ const billingStatus = tenant.billingStatus || tenant.billing_status || tenant.status;
536
+
537
+ console.log();
538
+ success("API key validated!");
539
+ console.log();
540
+ console.log(` ${colors.muted}Tenant:${colors.reset} ${tenant.name}`);
541
+ console.log(` ${colors.muted}Email:${colors.reset} ${tenant.email}`);
542
+ console.log(` ${colors.muted}Plan:${colors.reset} ${formatPlan(tenant.plan || "starter")}`);
543
+ console.log(` ${colors.muted}Status:${colors.reset} ${formatStatus(billingStatus)}`);
544
+ console.log();
545
+
546
+ // Check if payment needed
547
+ const unpaidStatuses = ['pending', 'incomplete', 'incomplete_expired', 'unpaid'];
548
+ if (unpaidStatuses.includes(billingStatus?.toLowerCase())) {
549
+ print(`${colors.warning}⚠ Subscription required${colors.reset}`, colors.bold);
550
+ console.log();
551
+ console.log(` Complete your subscription at: ${colors.accent}${WEB_URL}/checkout${colors.reset}`);
552
+ console.log();
553
+ }
554
+
555
+ // Show how to save the key
556
+ print("Save your key:", colors.bold);
557
+ print("─".repeat(40), colors.muted);
558
+ console.log(` Add to your shell config (~/.bashrc or ~/.zshrc):`);
559
+ console.log();
560
+ console.log(` ${colors.accent}export MOLT_TENANT_KEY="${key}"${colors.reset}`);
561
+ console.log();
562
+ console.log(` Then run: ${colors.accent}source ~/.bashrc${colors.reset}`);
563
+ console.log();
564
+
565
+ } catch (err) {
566
+ if (err.code === "ECONNREFUSED") {
567
+ print(`Cannot connect to API at ${API_URL}`, colors.error);
568
+ } else {
569
+ print(`Validation failed: ${err.message}`, colors.error);
570
+ }
571
+ console.log();
572
+ }
573
+
574
+ } else {
575
+ print("Invalid choice. Please run 'moltctl login' again.", colors.warning);
576
+ console.log();
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Checkout command - go directly to checkout/subscribe
582
+ */
583
+ async function cmdCheckout(plan) {
584
+ console.log(ASCII_LOGO_COMPACT);
585
+ console.log();
586
+
587
+ // Check if already subscribed
588
+ if (TENANT_KEY) {
589
+ try {
590
+ const tenant = await apiGet("/tenant/me");
591
+ const billingStatus = tenant.billingStatus || tenant.billing_status || tenant.status;
592
+
593
+ if (billingStatus === "active") {
594
+ success("You already have an active subscription!");
595
+ console.log();
596
+ console.log(` ${colors.muted}Plan:${colors.reset} ${formatPlan(tenant.plan || "starter")}`);
597
+ console.log();
598
+ console.log(` ${colors.info}To upgrade, run: ${colors.accent}moltctl upgrade${colors.reset}`);
599
+ console.log(` ${colors.info}To manage billing: ${colors.accent}${WEB_URL}/dashboard/subscription${colors.reset}`);
600
+ console.log();
601
+ return;
602
+ }
603
+ } catch {
604
+ // Key invalid, proceed to checkout
605
+ }
606
+ }
607
+
608
+ print("Plans", colors.bold);
609
+ print("─".repeat(40), colors.muted);
610
+ console.log(` ${colors.accent}starter${colors.reset} ${PLAN_PRICES.starter} — 1 agent, all tools, approvals`);
611
+ console.log(` ${colors.accent}team${colors.reset} ${PLAN_PRICES.team} — 5 agents, approvals`);
612
+ console.log(` ${colors.accent}business${colors.reset} ${PLAN_PRICES.business} — 25 agents, SIEM, RBAC`);
613
+ console.log(` ${colors.accent}enterprise${colors.reset} ${PLAN_PRICES.enterprise} — SSO, VPC, compliance`);
614
+ console.log();
615
+
616
+ // Build checkout URL
617
+ let checkoutUrl = `${WEB_URL}/checkout`;
618
+ if (plan && ['starter', 'team', 'business', 'enterprise'].includes(plan.toLowerCase())) {
619
+ if (plan.toLowerCase() === 'enterprise') {
620
+ print("Enterprise Plan", colors.bold);
621
+ print("─".repeat(40), colors.muted);
622
+ console.log(` Contact sales for custom pricing and features.`);
623
+ console.log();
624
+ console.log(` ${colors.info}→ Email: ${colors.accent}sales@mg.moltengine.com${colors.reset}`);
625
+ console.log();
626
+ return;
627
+ }
628
+ checkoutUrl += `?plan=${plan.toLowerCase()}`;
629
+ print(`Opening checkout for ${plan} plan...`, colors.info);
630
+ } else if (plan) {
631
+ print(`Unknown plan: ${plan}. Opening general checkout...`, colors.warning);
632
+ } else {
633
+ print("Opening checkout in your browser...", colors.info);
634
+ }
635
+
636
+ const opened = await openBrowser(checkoutUrl);
637
+
638
+ if (!opened) {
639
+ console.log();
640
+ console.log(` ${colors.muted}Could not open browser. Please visit:${colors.reset}`);
641
+ console.log(` ${colors.accent}${checkoutUrl}${colors.reset}`);
642
+ }
643
+
644
+ console.log();
645
+ print("Next steps:", colors.bold);
646
+ print("─".repeat(40), colors.muted);
647
+ console.log(` 1. Complete checkout via Stripe`);
648
+ console.log(` 2. Copy your API key from the success page`);
649
+ console.log(` 3. Set your environment variable:`);
650
+ console.log(` ${colors.accent}export MOLT_TENANT_KEY="your-key"${colors.reset}`);
651
+ console.log(` 4. Run ${colors.accent}moltctl status${colors.reset} to verify`);
652
+ console.log();
653
+ }
654
+
273
655
  /**
274
656
  * Display help with branding
275
657
  */
@@ -282,6 +664,11 @@ function showHelp() {
282
664
  console.log(` ${colors.primary}moltctl${colors.reset} <command> [options]`);
283
665
  console.log();
284
666
 
667
+ print("GETTING STARTED", colors.bold);
668
+ console.log(` ${colors.accent}login${colors.reset} Authenticate and get your API key`);
669
+ console.log(` ${colors.accent}checkout${colors.reset} Subscribe to a plan (opens browser)`);
670
+ console.log();
671
+
285
672
  print("COMMANDS", colors.bold);
286
673
  console.log(` ${colors.accent}status${colors.reset} Show status, usage, and pending approvals`);
287
674
  console.log(` ${colors.accent}whoami${colors.reset} Show current tenant and plan information`);
@@ -289,11 +676,19 @@ function showHelp() {
289
676
  console.log(` ${colors.accent}approvals${colors.reset} List and manage pending approvals`);
290
677
  console.log(` ${colors.accent}deployments${colors.reset} List recent deployments`);
291
678
  console.log(` ${colors.accent}upgrade${colors.reset} Upgrade to a higher plan`);
292
- console.log(` ${colors.accent}audit${colors.reset} Search and export audit logs (Team+)`);
293
- console.log(` ${colors.accent}policies${colors.reset} Manage security policies (Team+)`);
679
+ console.log(` ${colors.accent}audit${colors.reset} Search and export audit logs`);
680
+ console.log(` ${colors.accent}policies${colors.reset} Manage security policies`);
294
681
  console.log(` ${colors.accent}help${colors.reset} Show this help message`);
295
682
  console.log();
296
683
 
684
+ print("QUICK START", colors.bold);
685
+ console.log(` ${colors.muted}# New user? Start here:${colors.reset}`);
686
+ console.log(` ${colors.primary}moltctl${colors.reset} login`);
687
+ console.log();
688
+ console.log(` ${colors.muted}# Or go directly to checkout:${colors.reset}`);
689
+ console.log(` ${colors.primary}moltctl${colors.reset} checkout starter`);
690
+ console.log();
691
+
297
692
  print("APPROVALS", colors.bold);
298
693
  console.log(` ${colors.primary}moltctl${colors.reset} approvals list List pending approvals`);
299
694
  console.log(` ${colors.primary}moltctl${colors.reset} approvals approve <id> Approve a pending action`);
@@ -302,19 +697,7 @@ function showHelp() {
302
697
 
303
698
  print("ENVIRONMENT", colors.bold);
304
699
  console.log(` ${colors.muted}MOLTENGINE_API${colors.reset} API URL (default: http://localhost:8080)`);
305
- console.log(` ${colors.muted}MOLT_TENANT_KEY${colors.reset} Your tenant API key (required)`);
306
- console.log();
307
-
308
- print("EXAMPLES", colors.bold);
309
- console.log(` ${colors.muted}# Set up environment${colors.reset}`);
310
- console.log(` export MOLTENGINE_API="https://api.moltengine.com"`);
311
- console.log(` export MOLT_TENANT_KEY="your-tenant-key-here"`);
312
- console.log();
313
- console.log(` ${colors.muted}# Check status and pending approvals${colors.reset}`);
314
- console.log(` ${colors.primary}moltctl${colors.reset} status`);
315
- console.log();
316
- console.log(` ${colors.muted}# Approve a pending action${colors.reset}`);
317
- console.log(` ${colors.primary}moltctl${colors.reset} approvals approve 1`);
700
+ console.log(` ${colors.muted}MOLT_TENANT_KEY${colors.reset} Your tenant API key (from checkout)`);
318
701
  console.log();
319
702
  }
320
703
 
@@ -324,6 +707,11 @@ function showHelp() {
324
707
  async function cmdWhoami() {
325
708
  const data = await apiGet("/tenant/me");
326
709
 
710
+ // Check if payment is required
711
+ if (checkPaymentRequired(data)) {
712
+ return;
713
+ }
714
+
327
715
  printHeader("Tenant Information");
328
716
  console.log(` ${colors.muted}ID:${colors.reset} ${data.tenantId}`);
329
717
  console.log(` ${colors.muted}Name:${colors.reset} ${data.name}`);
@@ -362,9 +750,16 @@ async function cmdWhoami() {
362
750
  * Show tenant status with usage and approvals/blocked actions
363
751
  */
364
752
  async function cmdStatus() {
365
- // Fetch tenant info, usage, and limits in parallel
366
- const [tenant, usage, limits] = await Promise.all([
367
- apiGet("/tenant/me"),
753
+ // Fetch tenant info first to check payment status
754
+ const tenant = await apiGet("/tenant/me");
755
+
756
+ // Check if payment is required
757
+ if (checkPaymentRequired(tenant)) {
758
+ return;
759
+ }
760
+
761
+ // Fetch usage and limits in parallel
762
+ const [usage, limits] = await Promise.all([
368
763
  apiGet("/tenant/usage").catch(() => ({ runs: 0, events: 0, agents: 0 })),
369
764
  apiGet("/tenant/limits").catch(() => ({ runs: 100, events: 1000, agents: 1 }))
370
765
  ]);
@@ -387,16 +782,14 @@ async function cmdStatus() {
387
782
  }
388
783
 
389
784
  const plan = tenant.plan || "starter";
390
- const hasApprovals = plan !== "starter";
785
+ // All plans have approvals
391
786
 
392
787
  // Print logo
393
788
  console.log(ASCII_LOGO_COMPACT);
394
789
 
395
790
  // Tenant Status section
396
791
  printHeader("Tenant Status");
397
- const planDisplay = plan === "starter"
398
- ? `Starter (${PLAN_PRICES.starter}) — ${colors.warning}Safe Mode${colors.reset}`
399
- : `${plan.charAt(0).toUpperCase() + plan.slice(1)} (${PLAN_PRICES[plan] || ""})`;
792
+ const planDisplay = `${plan.charAt(0).toUpperCase() + plan.slice(1)} (${PLAN_PRICES[plan] || ""})`;
400
793
  console.log(` ${colors.muted}Plan:${colors.reset} ${planDisplay}`);
401
794
 
402
795
  const statusIcon = tenant.status === "active" ? "●" : "○";
@@ -424,67 +817,38 @@ async function cmdStatus() {
424
817
  const runsPct = limits.runs > 0 ? (usage.runs / limits.runs) * 100 : 0;
425
818
  const eventsPct = limits.events > 0 ? (usage.events / limits.events) * 100 : 0;
426
819
 
427
- // Approvals section (for Team+ plans)
428
- if (hasApprovals) {
429
- const pendingApprovals = approvals.filter(a => a.status === "pending");
430
- const pendingCount = pendingApprovals.length;
431
-
432
- console.log();
433
- if (pendingCount > 0) {
434
- print(`Approvals ${colors.warning}⚠ ${pendingCount} pending${colors.reset}`, colors.bold);
435
- } else {
436
- print(`Approvals ${colors.success}✓ none pending${colors.reset}`, colors.bold);
437
- }
438
- print("─".repeat(40), colors.muted);
439
-
440
- if (pendingCount > 0) {
441
- // Show up to 5 pending approvals
442
- const toShow = pendingApprovals.slice(0, 5);
443
- for (let i = 0; i < toShow.length; i++) {
444
- const a = toShow[i];
445
- const actionType = (a.action_type || "ACTION").toUpperCase().padEnd(5);
446
- const target = truncate(a.action_path || a.description || "unknown", 28);
447
- const time = formatRelativeTime(a.created_at);
448
- console.log(` ${colors.muted}[${i + 1}]${colors.reset} ${actionType} ${target.padEnd(28)} ${colors.muted}${time}${colors.reset}`);
449
- }
450
-
451
- if (pendingCount > 5) {
452
- console.log(` ${colors.muted}... and ${pendingCount - 5} more${colors.reset}`);
453
- }
454
-
455
- console.log();
456
- console.log(` ${colors.info}→ moltctl approvals approve <id>${colors.reset}`);
457
- console.log(` ${colors.info}→ moltctl approvals list --all${colors.reset}`);
458
- } else {
459
- console.log(` ${colors.muted}No actions waiting for approval${colors.reset}`);
460
- }
820
+ // Approvals section (available on all plans)
821
+ const pendingApprovals = approvals.filter(a => a.status === "pending");
822
+ const pendingCount = pendingApprovals.length;
823
+
824
+ console.log();
825
+ if (pendingCount > 0) {
826
+ print(`Approvals ${colors.warning}⚠ ${pendingCount} pending${colors.reset}`, colors.bold);
461
827
  } else {
462
- // Starter plan - show blocked actions
463
- const blockedCount = blockedActions.length || 0;
464
- const todayBlocked = usage.blocked_today || blockedCount || 0;
465
-
466
- console.log();
467
- if (todayBlocked > 0) {
468
- print(`Blocked Today ${colors.error}⛔ ${todayBlocked} actions${colors.reset}`, colors.bold);
469
- } else {
470
- print(`Blocked Today ${colors.success}✓ none${colors.reset}`, colors.bold);
828
+ print(`Approvals ${colors.success}✓ none pending${colors.reset}`, colors.bold);
829
+ }
830
+ print("─".repeat(40), colors.muted);
831
+
832
+ if (pendingCount > 0) {
833
+ // Show up to 5 pending approvals
834
+ const toShow = pendingApprovals.slice(0, 5);
835
+ for (let i = 0; i < toShow.length; i++) {
836
+ const a = toShow[i];
837
+ const actionType = (a.action_type || "ACTION").toUpperCase().padEnd(5);
838
+ const target = truncate(a.action_path || a.description || "unknown", 28);
839
+ const time = formatRelativeTime(a.created_at);
840
+ console.log(` ${colors.muted}[${i + 1}]${colors.reset} ${actionType} ${target.padEnd(28)} ${colors.muted}${time}${colors.reset}`);
471
841
  }
472
- print("─".repeat(40), colors.muted);
473
842
 
474
- if (todayBlocked > 0 && blockedActions.length > 0) {
475
- // Show recent blocked actions
476
- const toShow = blockedActions.slice(0, 3);
477
- for (const b of toShow) {
478
- const actionType = (b.action_type || "ACTION").toUpperCase().padEnd(5);
479
- const target = truncate(b.action_path || b.description || "unknown", 28);
480
- const time = formatRelativeTime(b.created_at);
481
- console.log(` ${actionType} ${target.padEnd(28)} ${colors.muted}${time}${colors.reset}`);
482
- }
483
- console.log();
843
+ if (pendingCount > 5) {
844
+ console.log(` ${colors.muted}... and ${pendingCount - 5} more${colors.reset}`);
484
845
  }
485
846
 
486
- console.log(` ${colors.muted}Risky actions are blocked in Safe Mode.${colors.reset}`);
487
- console.log(` ${colors.info}→ Upgrade to enable approvals: ${colors.accent}moltctl upgrade team${colors.reset}`);
847
+ console.log();
848
+ console.log(` ${colors.info}→ moltctl approvals approve <id>${colors.reset}`);
849
+ console.log(` ${colors.info}→ moltctl approvals list --all${colors.reset}`);
850
+ } else {
851
+ console.log(` ${colors.muted}No high-risk actions waiting for approval${colors.reset}`);
488
852
  }
489
853
 
490
854
  // Show warning if approaching limits
@@ -514,6 +878,12 @@ function truncate(str, len) {
514
878
  * List recent deployments
515
879
  */
516
880
  async function cmdDeployments() {
881
+ // First check payment status
882
+ const tenant = await apiGet("/tenant/me");
883
+ if (checkPaymentRequired(tenant)) {
884
+ return;
885
+ }
886
+
517
887
  const data = await apiGet("/tenant/deployments");
518
888
 
519
889
  printHeader("Recent Deployments");
@@ -540,8 +910,15 @@ async function cmdDeployments() {
540
910
  * Show detailed usage vs plan limits
541
911
  */
542
912
  async function cmdUsage() {
543
- const [tenant, usage, limits] = await Promise.all([
544
- apiGet("/tenant/me"),
913
+ // Fetch tenant info first to check payment status
914
+ const tenant = await apiGet("/tenant/me");
915
+
916
+ // Check if payment is required
917
+ if (checkPaymentRequired(tenant)) {
918
+ return;
919
+ }
920
+
921
+ const [usage, limits] = await Promise.all([
545
922
  apiGet("/tenant/usage").catch(() => ({ runs: 0, events: 0, agents: 0, approvals: 0 })),
546
923
  apiGet("/tenant/limits").catch(() => ({ runs: 100, events: 1000, agents: 1, approvals: 0, retention_days: 7 }))
547
924
  ]);
@@ -587,12 +964,41 @@ async function cmdUsage() {
587
964
  */
588
965
  async function cmdUpgrade(targetPlan) {
589
966
  const tenant = await apiGet("/tenant/me");
967
+
968
+ // Check if payment is required - but allow showing upgrade options for unpaid users
969
+ const billingStatus = tenant.billingStatus || tenant.billing_status || tenant.status;
970
+ const unpaidStatuses = ['pending', 'incomplete', 'incomplete_expired', 'unpaid'];
971
+
972
+ if (unpaidStatuses.includes(billingStatus?.toLowerCase())) {
973
+ // User hasn't subscribed yet - redirect to checkout
974
+ console.log(ASCII_LOGO_COMPACT);
975
+ console.log();
976
+ print(`${colors.warning}⚡ Subscription Required${colors.reset}`, colors.bold);
977
+ console.log();
978
+ console.log(` You need an active subscription to use Moltengine.`);
979
+ console.log();
980
+ print("Choose a Plan", colors.bold);
981
+ print("─".repeat(40), colors.muted);
982
+ console.log(` ${colors.accent}Starter${colors.reset} ${PLAN_PRICES.starter} — 1 agent, all tools, approvals`);
983
+ console.log(` ${colors.accent}Team${colors.reset} ${PLAN_PRICES.team} — 5 agents, approvals`);
984
+ console.log(` ${colors.accent}Business${colors.reset} ${PLAN_PRICES.business} — 25 agents, SIEM, RBAC`);
985
+ console.log(` ${colors.accent}Enterprise${colors.reset} ${PLAN_PRICES.enterprise} — SSO, VPC, compliance`);
986
+ console.log();
987
+
988
+ const checkoutUrl = targetPlan
989
+ ? `${WEB_URL}/checkout?plan=${targetPlan}`
990
+ : `${WEB_URL}/checkout`;
991
+ print(`${colors.info}→ Subscribe now: ${colors.accent}${checkoutUrl}${colors.reset}`, colors.bold);
992
+ console.log();
993
+ return;
994
+ }
995
+
590
996
  const currentPlan = tenant.plan || "starter";
591
997
 
592
998
  console.log(ASCII_LOGO_COMPACT);
593
999
 
594
1000
  const plans = [
595
- { id: "starter", price: "$49/mo", agents: 1, runs: "100/day", highlight: "Safe Mode" },
1001
+ { id: "starter", price: "$49/mo", agents: 1, runs: "100/day", highlight: "All Tools" },
596
1002
  { id: "team", price: "$299/mo", agents: 5, runs: "1,000/day", highlight: "Human-in-the-loop approvals" },
597
1003
  { id: "business", price: "$999/mo", agents: 25, runs: "10,000/day", highlight: "SIEM export, RBAC" },
598
1004
  { id: "enterprise", price: "Custom", agents: 100, runs: "100,000/day", highlight: "SSO, VPC, compliance" }
@@ -663,31 +1069,18 @@ async function cmdUpgrade(targetPlan) {
663
1069
 
664
1070
  /**
665
1071
  * Approvals command - list, approve, or deny pending approvals
1072
+ * Available on all plans (required for high-risk actions)
666
1073
  */
667
1074
  async function cmdApprovals(subcommand, id) {
668
1075
  const tenant = await apiGet("/tenant/me");
669
- const plan = tenant.plan || "starter";
670
1076
 
671
- // Check if plan supports approvals
672
- if (plan === "starter") {
673
- console.log(ASCII_LOGO_COMPACT);
674
- console.log();
675
- print(`${colors.error}⛔ Approvals require Team plan or higher${colors.reset}`, colors.bold);
676
- console.log();
677
- console.log(` Your current plan (Starter) runs in Safe Mode where`);
678
- console.log(` risky actions are automatically blocked.`);
679
- console.log();
680
- console.log(` ${colors.info}Team plan includes:${colors.reset}`);
681
- console.log(` • Human-in-the-loop approvals`);
682
- console.log(` • Audit log search & export`);
683
- console.log(` • Policy editor`);
684
- console.log(` • Email alerts`);
685
- console.log();
686
- console.log(` ${colors.info}→ Upgrade: ${colors.accent}moltctl upgrade team${colors.reset}`);
687
- console.log();
1077
+ // Check if payment is required
1078
+ if (checkPaymentRequired(tenant)) {
688
1079
  return;
689
1080
  }
690
1081
 
1082
+ // Approvals available on all plans
1083
+
691
1084
  // Handle subcommands
692
1085
  switch (subcommand) {
693
1086
  case "list":
@@ -779,30 +1172,19 @@ async function denyAction(id) {
779
1172
  }
780
1173
 
781
1174
  /**
782
- * Audit command - search and export audit logs (Team+ only)
1175
+ * Audit command - search and export audit logs
1176
+ * Available on all plans
783
1177
  */
784
1178
  async function cmdAudit(subcommand, ...args) {
785
1179
  const tenant = await apiGet("/tenant/me");
786
- const plan = tenant.plan || "starter";
787
1180
 
788
- if (plan === "starter") {
789
- console.log(ASCII_LOGO_COMPACT);
790
- console.log();
791
- print(`${colors.error}⛔ Audit search requires Team plan or higher${colors.reset}`, colors.bold);
792
- console.log();
793
- console.log(` Your current plan (Starter) stores audit logs but`);
794
- console.log(` search and export require an upgrade.`);
795
- console.log();
796
- console.log(` ${colors.info}Team plan includes:${colors.reset}`);
797
- console.log(` • Audit log search`);
798
- console.log(` • CSV/JSON export`);
799
- console.log(` • 90-day retention (vs 7 days)`);
800
- console.log();
801
- console.log(` ${colors.info}→ Upgrade: ${colors.accent}moltctl upgrade team${colors.reset}`);
802
- console.log();
1181
+ // Check if payment is required
1182
+ if (checkPaymentRequired(tenant)) {
803
1183
  return;
804
1184
  }
805
1185
 
1186
+ // Audit available on all plans
1187
+
806
1188
  // Handle subcommands
807
1189
  switch (subcommand) {
808
1190
  case "search":
@@ -939,30 +1321,19 @@ async function auditStats() {
939
1321
  }
940
1322
 
941
1323
  /**
942
- * Policies command - manage security policies (Team+ only)
1324
+ * Policies command - manage security policies
1325
+ * Available on all plans
943
1326
  */
944
1327
  async function cmdPolicies(subcommand, ...args) {
945
1328
  const tenant = await apiGet("/tenant/me");
946
- const plan = tenant.plan || "starter";
947
1329
 
948
- if (plan === "starter") {
949
- console.log(ASCII_LOGO_COMPACT);
950
- console.log();
951
- print(`${colors.error}⛔ Policy editor requires Team plan or higher${colors.reset}`, colors.bold);
952
- console.log();
953
- console.log(` Your current plan (Starter) uses default security`);
954
- console.log(` policies. Custom policies require an upgrade.`);
955
- console.log();
956
- console.log(` ${colors.info}Team plan includes:${colors.reset}`);
957
- console.log(` • Tool allowlist/blocklist`);
958
- console.log(` • Network domain policies`);
959
- console.log(` • Filesystem access rules`);
960
- console.log();
961
- console.log(` ${colors.info}→ Upgrade: ${colors.accent}moltctl upgrade team${colors.reset}`);
962
- console.log();
1330
+ // Check if payment is required
1331
+ if (checkPaymentRequired(tenant)) {
963
1332
  return;
964
1333
  }
965
1334
 
1335
+ // Policy editor available on all plans
1336
+
966
1337
  // Handle subcommands
967
1338
  switch (subcommand) {
968
1339
  case "list":
@@ -1008,9 +1379,9 @@ async function policiesList() {
1008
1379
  console.log(` ${colors.muted}Using secure defaults${colors.reset}`);
1009
1380
  console.log();
1010
1381
  console.log(` ${colors.info}Default policy:${colors.reset}`);
1011
- console.log(` • Dangerous tools blocked (shell, browser)`);
1382
+ console.log(` • All tools available (high-risk actions require approval)`);
1012
1383
  console.log(` • Network requests allowed (except blocked domains)`);
1013
- console.log(` • Filesystem writes restricted to /data/*, /exports/*`);
1384
+ console.log(` • Sandboxing enabled by default (user-configurable)`);
1014
1385
  console.log();
1015
1386
  return;
1016
1387
  }
@@ -1219,6 +1590,12 @@ async function policiesBlockDomain(domain) {
1219
1590
  */
1220
1591
  async function cmdExport(subcommand, ...args) {
1221
1592
  const tenant = await apiGet("/tenant/me");
1593
+
1594
+ // Check if payment is required
1595
+ if (checkPaymentRequired(tenant)) {
1596
+ return;
1597
+ }
1598
+
1222
1599
  const plan = tenant.plan || "starter";
1223
1600
 
1224
1601
  if (plan === "starter" || plan === "team") {
@@ -1410,6 +1787,12 @@ async function exportHistory(configId) {
1410
1787
  */
1411
1788
  async function cmdUsers(subcommand, ...args) {
1412
1789
  const tenant = await apiGet("/tenant/me");
1790
+
1791
+ // Check if payment is required
1792
+ if (checkPaymentRequired(tenant)) {
1793
+ return;
1794
+ }
1795
+
1413
1796
  const plan = tenant.plan || "starter";
1414
1797
 
1415
1798
  if (plan === "starter" || plan === "team") {
@@ -1551,6 +1934,12 @@ async function usersRemove(userId) {
1551
1934
  */
1552
1935
  async function cmdEnv(subcommand, ...args) {
1553
1936
  const tenant = await apiGet("/tenant/me");
1937
+
1938
+ // Check if payment is required
1939
+ if (checkPaymentRequired(tenant)) {
1940
+ return;
1941
+ }
1942
+
1554
1943
  const plan = tenant.plan || "starter";
1555
1944
 
1556
1945
  if (plan === "starter" || plan === "team") {
@@ -1702,6 +2091,15 @@ async function main() {
1702
2091
  console.log();
1703
2092
 
1704
2093
  switch (command) {
2094
+ case "login":
2095
+ await cmdLogin();
2096
+ break;
2097
+
2098
+ case "checkout":
2099
+ case "subscribe":
2100
+ await cmdCheckout(args[0]);
2101
+ break;
2102
+
1705
2103
  case "whoami":
1706
2104
  await cmdWhoami();
1707
2105
  break;